From e45103be0e61bc71f54f424be4023eb6faa8a1b1 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Mon, 30 Mar 2026 23:36:31 -0700 Subject: [PATCH 01/84] infra: nvkind GPU cluster with KAI scheduler, KubeRay, and examples Local K8s GPU dev environment using nvkind (NVIDIA's kind wrapper): - nvkind cluster setup scripts (install-nvkind.sh, create-cluster.sh) - Custom config template with extraMounts for dev code mounting - Helmfile with kind/prod environments (device plugin vs GPU operator) - KAI scheduler for gang scheduling, KubeRay for RayCluster management - Example manifests: gang-scheduled pods, RayClusters, SFT RayJobs - SETUP.md with prerequisites, quick start, and architecture docs Tested: SFT RayJob (train/loss 4.06 < 5.9), KAI all-or-nothing gang scheduling, two simultaneous 1-GPU SFT jobs. Signed-off-by: Terry Kong --- infra/examples/kai-queue.yaml | 40 +++++ infra/examples/kai_scheduled_pods.yaml | 47 ++++++ infra/examples/kai_scheduled_rayclusters.yaml | 111 +++++++++++++ infra/examples/kai_scheduled_sft.yaml | 127 +++++++++++++++ infra/examples/raycluster-blocker.yaml | 17 ++ infra/examples/sft_rayjob.yaml | 83 ++++++++++ infra/helm/helmfile.yaml | 70 ++++++++ infra/helm/values/gpu-operator.yaml | 4 + infra/helm/values/kai-scheduler.yaml | 2 + infra/helm/values/kuberay-operator.yaml | 2 + infra/helm/values/nvidia-device-plugin.yaml | 18 +++ infra/kind/SETUP.md | 149 ++++++++++++++++++ infra/kind/create-cluster.sh | 72 +++++++++ infra/kind/get-helm.sh | 36 +++++ infra/kind/get-kubectl.sh | 71 +++++++++ infra/kind/install-nvkind.sh | 43 +++++ infra/kind/nvkind-config-template.yaml | 52 ++++++ infra/kind/nvkind-config-values-dev.yaml | 7 + infra/kind/nvkind-config-values.yaml | 2 + 19 files changed, 953 insertions(+) create mode 100644 infra/examples/kai-queue.yaml create mode 100644 infra/examples/kai_scheduled_pods.yaml create mode 100644 infra/examples/kai_scheduled_rayclusters.yaml create mode 100644 infra/examples/kai_scheduled_sft.yaml create mode 100644 infra/examples/raycluster-blocker.yaml create mode 100644 infra/examples/sft_rayjob.yaml create mode 100644 infra/helm/helmfile.yaml create mode 100644 infra/helm/values/gpu-operator.yaml create mode 100644 infra/helm/values/kai-scheduler.yaml create mode 100644 infra/helm/values/kuberay-operator.yaml create mode 100644 infra/helm/values/nvidia-device-plugin.yaml create mode 100644 infra/kind/SETUP.md create mode 100644 infra/kind/create-cluster.sh create mode 100644 infra/kind/get-helm.sh create mode 100644 infra/kind/get-kubectl.sh create mode 100644 infra/kind/install-nvkind.sh create mode 100644 infra/kind/nvkind-config-template.yaml create mode 100644 infra/kind/nvkind-config-values-dev.yaml create mode 100644 infra/kind/nvkind-config-values.yaml diff --git a/infra/examples/kai-queue.yaml b/infra/examples/kai-queue.yaml new file mode 100644 index 00000000000..1ca9912ea19 --- /dev/null +++ b/infra/examples/kai-queue.yaml @@ -0,0 +1,40 @@ +# KAI Scheduler queue hierarchy for local dev. +# Unlimited quotas — appropriate for single-user kind clusters. +apiVersion: scheduling.run.ai/v2 +kind: Queue +metadata: + name: department-1 +spec: + resources: + gpu: + quota: -1 + limit: -1 + overQuotaWeight: 1 + cpu: + quota: -1 + limit: -1 + overQuotaWeight: 1 + memory: + quota: -1 + limit: -1 + overQuotaWeight: 1 +--- +apiVersion: scheduling.run.ai/v2 +kind: Queue +metadata: + name: team-a +spec: + parentQueue: department-1 + resources: + gpu: + quota: -1 + limit: -1 + overQuotaWeight: 1 + cpu: + quota: -1 + limit: -1 + overQuotaWeight: 1 + memory: + quota: -1 + limit: -1 + overQuotaWeight: 1 diff --git a/infra/examples/kai_scheduled_pods.yaml b/infra/examples/kai_scheduled_pods.yaml new file mode 100644 index 00000000000..94a7a836353 --- /dev/null +++ b/infra/examples/kai_scheduled_pods.yaml @@ -0,0 +1,47 @@ +# Gang-schedule two GPU pods via KAI scheduler. +# Both pods are scheduled simultaneously or not at all. +apiVersion: scheduling.run.ai/v2alpha2 +kind: PodGroup +metadata: + name: gpu-test-group +spec: + minMember: 2 + queue: team-a +--- +apiVersion: v1 +kind: Pod +metadata: + name: gpu-test-0 + labels: + kai.scheduler/queue: team-a + annotations: + pod-group-name: gpu-test-group +spec: + schedulerName: kai-scheduler + restartPolicy: Never + containers: + - name: gpu-test + image: nvidia/cuda:12.8.1-base-ubuntu24.04 + command: ["nvidia-smi"] + resources: + limits: + nvidia.com/gpu: "1" +--- +apiVersion: v1 +kind: Pod +metadata: + name: gpu-test-1 + labels: + kai.scheduler/queue: team-a + annotations: + pod-group-name: gpu-test-group +spec: + schedulerName: kai-scheduler + restartPolicy: Never + containers: + - name: gpu-test + image: nvidia/cuda:12.8.1-base-ubuntu24.04 + command: ["nvidia-smi"] + resources: + limits: + nvidia.com/gpu: "1" diff --git a/infra/examples/kai_scheduled_rayclusters.yaml b/infra/examples/kai_scheduled_rayclusters.yaml new file mode 100644 index 00000000000..2467f918284 --- /dev/null +++ b/infra/examples/kai_scheduled_rayclusters.yaml @@ -0,0 +1,111 @@ +# Gang-schedule two RayClusters via KAI scheduler. +# Each cluster gets 1 GPU worker. Both clusters should come up simultaneously. +apiVersion: ray.io/v1 +kind: RayCluster +metadata: + name: raycluster-a + labels: + kai.scheduler/queue: team-a +spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: + object-store-memory: "100000000" # 100MB, minimum for kind + template: + spec: + schedulerName: kai-scheduler + containers: + - name: ray-head + image: rayproject/ray:2.52.0 + resources: + limits: + cpu: "1" + memory: "2Gi" + requests: + cpu: "500m" + memory: "512Mi" + ports: + - containerPort: 6379 + name: gcs-server + - containerPort: 8265 + name: dashboard + - containerPort: 10001 + name: client + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: + num-gpus: "1" + object-store-memory: "100000000" + template: + spec: + schedulerName: kai-scheduler + containers: + - name: ray-worker + image: rayproject/ray:2.52.0-gpu + resources: + limits: + cpu: "1" + memory: "2Gi" + nvidia.com/gpu: "1" + requests: + cpu: "500m" + memory: "512Mi" + nvidia.com/gpu: "1" +--- +apiVersion: ray.io/v1 +kind: RayCluster +metadata: + name: raycluster-b + labels: + kai.scheduler/queue: team-a +spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: + object-store-memory: "100000000" + template: + spec: + schedulerName: kai-scheduler + containers: + - name: ray-head + image: rayproject/ray:2.52.0 + resources: + limits: + cpu: "1" + memory: "2Gi" + requests: + cpu: "500m" + memory: "512Mi" + ports: + - containerPort: 6379 + name: gcs-server + - containerPort: 8265 + name: dashboard + - containerPort: 10001 + name: client + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: + num-gpus: "1" + object-store-memory: "100000000" + template: + spec: + schedulerName: kai-scheduler + containers: + - name: ray-worker + image: rayproject/ray:2.52.0-gpu + resources: + limits: + cpu: "1" + memory: "2Gi" + nvidia.com/gpu: "1" + requests: + cpu: "500m" + memory: "512Mi" + nvidia.com/gpu: "1" diff --git a/infra/examples/kai_scheduled_sft.yaml b/infra/examples/kai_scheduled_sft.yaml new file mode 100644 index 00000000000..ea678b63df8 --- /dev/null +++ b/infra/examples/kai_scheduled_sft.yaml @@ -0,0 +1,127 @@ +# Gang-schedule two 1-GPU SFT RayJobs via KAI scheduler. +# Each runs SFT with cluster.gpus_per_node=1, then tears down. +apiVersion: ray.io/v1 +kind: RayJob +metadata: + name: sft-job-a + labels: + kai.scheduler/queue: team-a +spec: + entrypoint: "cd /opt/nemo-rl && bash tests/functional/sft.sh cluster.gpus_per_node=1" + # HTTPMode required with KAI scheduler (K8sJobMode breaks gang scheduling). + submissionMode: HTTPMode + shutdownAfterJobFinishes: true + ttlSecondsAfterFinished: 60 + rayClusterSpec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: + object-store-memory: "200000000" + template: + spec: + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + containers: + - name: ray-head + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + resources: + limits: + cpu: "4" + memory: "16Gi" + requests: + cpu: "1" + memory: "4Gi" + ports: + - containerPort: 6379 + name: gcs-server + - containerPort: 8265 + name: dashboard + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: + num-gpus: "1" + object-store-memory: "200000000" + template: + spec: + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + containers: + - name: ray-worker + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + resources: + limits: + cpu: "4" + memory: "16Gi" + nvidia.com/gpu: "1" + requests: + cpu: "1" + memory: "4Gi" + nvidia.com/gpu: "1" +--- +apiVersion: ray.io/v1 +kind: RayJob +metadata: + name: sft-job-b + labels: + kai.scheduler/queue: team-a +spec: + entrypoint: "cd /opt/nemo-rl && bash tests/functional/sft.sh cluster.gpus_per_node=1" + # HTTPMode required with KAI scheduler (K8sJobMode breaks gang scheduling). + submissionMode: HTTPMode + shutdownAfterJobFinishes: true + ttlSecondsAfterFinished: 60 + rayClusterSpec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: + object-store-memory: "200000000" + template: + spec: + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + containers: + - name: ray-head + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + resources: + limits: + cpu: "4" + memory: "16Gi" + requests: + cpu: "1" + memory: "4Gi" + ports: + - containerPort: 6379 + name: gcs-server + - containerPort: 8265 + name: dashboard + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: + num-gpus: "1" + object-store-memory: "200000000" + template: + spec: + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + containers: + - name: ray-worker + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + resources: + limits: + cpu: "4" + memory: "16Gi" + nvidia.com/gpu: "1" + requests: + cpu: "1" + memory: "4Gi" + nvidia.com/gpu: "1" diff --git a/infra/examples/raycluster-blocker.yaml b/infra/examples/raycluster-blocker.yaml new file mode 100644 index 00000000000..c83a4013b64 --- /dev/null +++ b/infra/examples/raycluster-blocker.yaml @@ -0,0 +1,17 @@ +# A simple pod that occupies 1 GPU, used to test KAI gang scheduling. +# Uses the default scheduler (not KAI) so it won't be preempted. +# Deploy this first, then try to deploy kai_scheduled_rayclusters.yaml. +# With only 1 GPU free, KAI should hold BOTH rayclusters pending (all-or-nothing). +apiVersion: v1 +kind: Pod +metadata: + name: gpu-blocker +spec: + restartPolicy: Never + containers: + - name: blocker + image: nvidia/cuda:12.8.1-base-ubuntu24.04 + command: ["sleep", "infinity"] + resources: + limits: + nvidia.com/gpu: "1" diff --git a/infra/examples/sft_rayjob.yaml b/infra/examples/sft_rayjob.yaml new file mode 100644 index 00000000000..8424b56b0dd --- /dev/null +++ b/infra/examples/sft_rayjob.yaml @@ -0,0 +1,83 @@ +# 2-GPU RayJob for running nemo-rl SFT training. +# Creates a cluster, runs SFT, then tears down automatically. +apiVersion: ray.io/v1 +kind: RayJob +metadata: + name: sft-job + labels: + kai.scheduler/queue: team-a +spec: + entrypoint: "cd /opt/nemo-rl && bash tests/functional/sft.sh" + # HTTPMode is required with KAI scheduler — K8sJobMode creates a separate + # submitter pod after the cluster is ready, which breaks gang scheduling. + submissionMode: HTTPMode + shutdownAfterJobFinishes: true + ttlSecondsAfterFinished: 60 + rayClusterSpec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: + object-store-memory: "200000000" + template: + spec: + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + containers: + - name: ray-head + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + resources: + limits: + cpu: "4" + memory: "16Gi" + requests: + cpu: "1" + memory: "4Gi" + ports: + - containerPort: 6379 + name: gcs-server + - containerPort: 8265 + name: dashboard + - containerPort: 10001 + name: client + volumeMounts: + - name: nemo-rl-src + mountPath: /workspace/nemo-rl + volumes: + - name: nemo-rl-src + hostPath: + path: /workspace/nemo-rl + type: DirectoryOrCreate + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: + num-gpus: "2" + object-store-memory: "200000000" + template: + spec: + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + containers: + - name: ray-worker + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + resources: + limits: + cpu: "8" + memory: "32Gi" + nvidia.com/gpu: "2" + requests: + cpu: "2" + memory: "8Gi" + nvidia.com/gpu: "2" + volumeMounts: + - name: nemo-rl-src + mountPath: /workspace/nemo-rl + volumes: + - name: nemo-rl-src + hostPath: + path: /workspace/nemo-rl + type: DirectoryOrCreate diff --git a/infra/helm/helmfile.yaml b/infra/helm/helmfile.yaml new file mode 100644 index 00000000000..597b6eadf09 --- /dev/null +++ b/infra/helm/helmfile.yaml @@ -0,0 +1,70 @@ +environments: + kind: + values: + - gpu_backend: device-plugin # nvkind handles toolkit/runtime, only need device plugin + prod: + values: + - gpu_backend: gpu-operator # full operator manages driver, toolkit, device plugin, NFD + +--- + +repositories: +- name: nvdp + url: https://nvidia.github.io/k8s-device-plugin +- name: nvidia + url: https://helm.ngc.nvidia.com/nvidia +- name: kuberay + url: https://ray-project.github.io/kuberay-helm/ +- name: prometheus-community + url: https://prometheus-community.github.io/helm-charts + +releases: +# +# GPU: device plugin only (kind) — nvkind handles the rest +# +{{ if eq .Environment.Values.gpu_backend "device-plugin" }} +- name: nvidia-device-plugin + namespace: nvidia + createNamespace: true + chart: nvdp/nvidia-device-plugin + wait: true + values: + - values/nvidia-device-plugin.yaml +{{ end }} + +# +# GPU: full operator (prod) — manages driver, toolkit, device plugin, NFD, DCGM +# +{{ if eq .Environment.Values.gpu_backend "gpu-operator" }} +- name: gpu-operator + namespace: gpu-operator + createNamespace: true + chart: nvidia/gpu-operator + wait: true + waitForJobs: true + values: + - values/gpu-operator.yaml +{{ end }} + +# +# KAI Scheduler: gang scheduling for GPU workloads +# +- name: kai-scheduler + namespace: kai-scheduler + createNamespace: true + chart: https://github.com/NVIDIA/KAI-Scheduler/releases/download/v0.13.4/kai-scheduler-v0.13.4.tgz + wait: true + values: + - values/kai-scheduler.yaml + +# +# KubeRay Operator: manages RayCluster CRDs, integrated with KAI for gang scheduling +# +- name: kuberay-operator + namespace: kuberay-system + createNamespace: true + chart: kuberay/kuberay-operator + version: 1.6.0 + wait: true + values: + - values/kuberay-operator.yaml diff --git a/infra/helm/values/gpu-operator.yaml b/infra/helm/values/gpu-operator.yaml new file mode 100644 index 00000000000..962fe5145f8 --- /dev/null +++ b/infra/helm/values/gpu-operator.yaml @@ -0,0 +1,4 @@ +# GPU Operator values for production clusters. +# Set driver.enabled=false if the host already has the NVIDIA driver installed. +driver: + enabled: true diff --git a/infra/helm/values/kai-scheduler.yaml b/infra/helm/values/kai-scheduler.yaml new file mode 100644 index 00000000000..0bdd979ae9d --- /dev/null +++ b/infra/helm/values/kai-scheduler.yaml @@ -0,0 +1,2 @@ +defaultQueue: + createDefaultQueue: true diff --git a/infra/helm/values/kuberay-operator.yaml b/infra/helm/values/kuberay-operator.yaml new file mode 100644 index 00000000000..1164f666b15 --- /dev/null +++ b/infra/helm/values/kuberay-operator.yaml @@ -0,0 +1,2 @@ +batchScheduler: + name: kai-scheduler diff --git a/infra/helm/values/nvidia-device-plugin.yaml b/infra/helm/values/nvidia-device-plugin.yaml new file mode 100644 index 00000000000..e2c13125d4d --- /dev/null +++ b/infra/helm/values/nvidia-device-plugin.yaml @@ -0,0 +1,18 @@ +# NOTE: These overrides are kind-specific. On a real cluster with the GPU Operator, +# you would NOT use this file at all — the GPU Operator deploys its own device plugin. + +# Override default affinity which requires NFD labels (not available in kind). +# On a real cluster, NFD is deployed by the GPU Operator, so the default affinity works. +affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/os + operator: In + values: + - linux + +# The device plugin pod needs the nvidia runtime to access NVML for GPU discovery. +# On a real cluster, the GPU Operator handles this automatically. +runtimeClassName: nvidia diff --git a/infra/kind/SETUP.md b/infra/kind/SETUP.md new file mode 100644 index 00000000000..9fb97542931 --- /dev/null +++ b/infra/kind/SETUP.md @@ -0,0 +1,149 @@ +# Local K8s GPU Development Environment + +nvkind-based Kubernetes cluster with NVIDIA GPU support, KAI scheduler (gang scheduling), and KubeRay. + +## Prerequisites + +- Docker with systemd cgroup driver and **cgroup v2** +- NVIDIA driver installed on host (`nvidia-smi` works) +- `nvidia-container-toolkit` installed on host +- `go` (for nvkind installation) +- `helmfile` (install: `curl -sSL https://github.com/helmfile/helmfile/releases/latest/download/helmfile_$(uname -s | tr '[:upper:]' '[:lower:]')_amd64.tar.gz | tar xz -C ~/bin helmfile`) + +### One-time host setup (requires sudo) + +```sh +# Set nvidia as Docker's default runtime and enable CDI +sudo nvidia-ctk runtime configure --runtime=docker --set-as-default --cdi.enabled +sudo nvidia-ctk config --set accept-nvidia-visible-devices-as-volume-mounts=true --in-place +sudo systemctl restart docker + +# Verify +docker info | grep "Default Runtime" # should show "nvidia" +stat -fc %T /sys/fs/cgroup/ # should show "cgroup2fs" +``` + +## Quick Start (local kind cluster) + +```sh +# 1. Install tools +cd infra/kind +bash install-nvkind.sh +bash get-kubectl.sh +bash get-helm.sh + +# 2. Create cluster (all host GPUs exposed to a single worker node) +bash create-cluster.sh + +# 3. Deploy infrastructure +cd ../helm +helmfile -e kind sync + +# 4. Create KAI scheduler queues +kubectl apply -f ../examples/kai-queue.yaml + +# 5. Test: gang-schedule two GPU pods +kubectl apply -f ../examples/kai_scheduled_pods.yaml +kubectl get pods -w # both go Running at the same time +kubectl logs gpu-test-0 # nvidia-smi output +kubectl delete -f ../examples/kai_scheduled_pods.yaml + +# 6. Test: gang-schedule two RayClusters (each with 1 GPU worker) +kubectl apply -f ../examples/kai_scheduled_rayclusters.yaml +kubectl get rayclusters -w # both become "ready" +kubectl delete -f ../examples/kai_scheduled_rayclusters.yaml +``` + +## Deploy on a real cluster + +```sh +cd infra/helm +helmfile -e prod sync +``` + +This installs the full **GPU Operator** (instead of just the device plugin) along with KAI scheduler and KubeRay. The GPU Operator manages the NVIDIA driver, container toolkit, device plugin, NFD, and DCGM exporter. + +Set `driver.enabled=false` in `values/gpu-operator.yaml` if the cluster nodes already have the NVIDIA driver installed. + +## Helmfile environments + +| Environment | GPU component | Use case | +|-------------|---------------|----------| +| `kind` | nvidia-device-plugin | Local dev — nvkind handles toolkit/runtime | +| `prod` | gpu-operator (full) | Real clusters — operator manages everything | + +Both environments include KAI scheduler and KubeRay operator. + +## Tear down (kind only) + +```sh +kind delete cluster --name nemo-rl +``` + +## Architecture + +``` +infra/ +├── kind/ # Cluster setup (local dev only) +│ ├── create-cluster.sh # Creates nvkind cluster +│ ├── install-nvkind.sh # Installs kind + nvkind +│ ├── get-kubectl.sh / get-helm.sh # Tool installers +│ ├── nvkind-config-values.yaml # Default: workers with all GPUs +│ ├── nvkind-config-values-dev.yaml # Dev: + local code mount +│ └── nvkind-config-template.yaml # Custom template with extraMounts +├── helm/ # Infrastructure (helmfile) +│ ├── helmfile.yaml # environments: kind, prod +│ └── values/ +│ ├── nvidia-device-plugin.yaml # kind only +│ ├── gpu-operator.yaml # prod only +│ ├── kai-scheduler.yaml +│ └── kuberay-operator.yaml +└── examples/ + ├── kai-queue.yaml # KAI queue hierarchy + ├── kai_scheduled_pods.yaml # Gang-scheduled GPU test pods + ├── kai_scheduled_rayclusters.yaml # Gang-scheduled RayClusters + ├── kai_scheduled_sft.yaml # Two gang-scheduled SFT RayJobs + ├── sft_rayjob.yaml # 2-GPU SFT RayJob + ├── raycluster-blocker.yaml # GPU blocker for testing KAI + ├── gym_standalone_config.yaml # Gym standalone server config + ├── disagg_rl_raycluster.yaml # Disagg RL cluster + peer-watcher + ├── disagg_gym_raycluster.yaml # Disagg Gym cluster + peer-watcher + ├── endpoint-registry-rbac.yaml # RBAC for ConfigMap endpoint registry + └── peer-watcher.py # Sidecar for failure cascading +``` + +## Notes + +- **nvkind vs vanilla kind**: nvkind automates GPU device injection, nvidia-container-toolkit installation inside nodes, containerd nvidia runtime configuration, and RuntimeClass registration. +- **nvidia-device-plugin** (kind only): The full GPU Operator fails in kind because its driver validation doesn't work inside kind nodes. The lightweight device plugin with CDI discovery is sufficient since nvkind handles the runtime setup. On a real cluster, use the full GPU Operator (`helmfile -e prod sync`). +- **Device plugin kind overrides**: Affinity is overridden because NFD isn't installed in kind. `runtimeClassName: nvidia` is set so the plugin pod gets NVIDIA libraries injected for NVML discovery. Neither override is needed on a real cluster. +- **KAI scheduler** creates PodGroups automatically for recognized workload types (RayCluster, Job, PyTorchJob, etc.). For bare pods, create a PodGroup manually and annotate pods with `pod-group-name`. +- **RayJob** (not RayCluster) is preferred for batch workloads — it auto-tears down the cluster after the job finishes, avoiding stale Ray state. Requires `submissionMode: HTTPMode` for KAI compatibility. + +## Failure cascading for disaggregated Gym + +The disaggregated RL/Gym manifests include a **peer-watcher sidecar** on each head pod that monitors the peer cluster via the K8s API. If the peer is deleted, fails, or signals an error via the ConfigMap, the watcher tears down both clusters to release resources. + +- `peer-watcher.py` — Python sidecar script (deployed as a ConfigMap) +- Monitors: peer RayCluster status + ConfigMap `error` key +- `MAX_PEER_FAILURES` (default 3) consecutive failures before teardown +- Applications can signal errors via `K8sEndpointRegistry.signal_error("message")` + +Setup: +```sh +kubectl create configmap peer-watcher-script --from-file=peer-watcher.py=infra/examples/peer-watcher.py +kubectl apply -f infra/examples/endpoint-registry-rbac.yaml +``` + +## TODO: Log persistence + +Currently, logs are lost when RayJob pods are cleaned up (`ttlSecondsAfterFinished`). Two levels of log persistence are needed: + +1. **Container stdout/stderr** (`kubectl logs`): Captured by containerd at `/var/log/pods/` on the node, but not queryable after pod deletion. +2. **Ray file logs** (`/tmp/ray/session_*/logs/`): Worker/driver logs, system logs — not sent to stdout at all. + +**Planned approach**: Deploy a Loki stack (Loki + Promtail + Grafana) via helmfile for both `kind` and `prod` environments: +- Promtail DaemonSet captures container stdout/stderr from each node, auto-labels with K8s metadata (namespace, pod, job name) +- Fluent Bit sidecar in each RayJob pod tails `/tmp/ray` logs and ships to Loki +- Grafana UI for querying logs by job name, time range, and content (LogQL) +- kind: Loki stores on local PVC; prod: Loki stores on S3/GCS diff --git a/infra/kind/create-cluster.sh b/infra/kind/create-cluster.sh new file mode 100644 index 00000000000..82164731160 --- /dev/null +++ b/infra/kind/create-cluster.sh @@ -0,0 +1,72 @@ +#!/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. +# +# Creates an nvkind cluster with GPU support. +# Prerequisites: +# - kind and nvkind installed (see install-nvkind.sh) +# - NVIDIA driver + nvidia-container-toolkit on the host +# - Docker running with systemd cgroup driver +# +# One-time host setup (run manually before first use): +# sudo nvidia-ctk runtime configure --runtime=docker --set-as-default --cdi.enabled +# sudo nvidia-ctk config --set accept-nvidia-visible-devices-as-volume-mounts=true --in-place +# sudo systemctl restart docker + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +set -eoux pipefail + +KIND_CLUSTER_NAME=${KIND_CLUSTER_NAME:-nemo-rl} +CONFIG_VALUES=${CONFIG_VALUES:-$SCRIPT_DIR/nvkind-config-values.yaml} + +echo "======================" +echo "Existing kind clusters" +echo "======================" +kind get clusters || true + +# Build nvkind args. Use custom template if it exists alongside the values file. +NVKIND_ARGS=(--name "${KIND_CLUSTER_NAME}" --config-values "$CONFIG_VALUES") +CONFIG_TEMPLATE="$SCRIPT_DIR/nvkind-config-template.yaml" +if [[ -f "$CONFIG_TEMPLATE" && "$CONFIG_VALUES" != "$SCRIPT_DIR/nvkind-config-values.yaml" ]]; then + echo "Using custom config template: $CONFIG_TEMPLATE" + NVKIND_ARGS+=(--config-template "$CONFIG_TEMPLATE") +fi + +# nvkind may fail at the /proc/driver/nvidia patching step if the host +# doesn't have a mounted /proc/driver/nvidia (non-MIG setups). This is +# non-fatal — the cluster and GPU access still work. We catch the error +# and verify the cluster came up. +nvkind cluster create "${NVKIND_ARGS[@]}" || true + +# nvkind installs the nvidia-container-toolkit and configures containerd +# inside worker nodes, but containerd needs a restart to pick up the config. +echo "Restarting containerd on worker nodes..." +for worker in $(docker ps --format '{{.Names}}' | grep -E "${KIND_CLUSTER_NAME}-worker"); do + docker exec "$worker" systemctl restart containerd +done + +echo "Waiting for nodes to become Ready..." +kubectl wait --for=condition=ready nodes --all --timeout=120s + +echo "======================" +echo "Verifying cluster..." +echo "======================" +docker ps +kubectl get nodes -o wide +kubectl get pods -A + +echo "" +echo "Cluster '${KIND_CLUSTER_NAME}' is ready." +echo "Next: cd ../helm && helmfile sync" diff --git a/infra/kind/get-helm.sh b/infra/kind/get-helm.sh new file mode 100644 index 00000000000..a280a395bcc --- /dev/null +++ b/infra/kind/get-helm.sh @@ -0,0 +1,36 @@ +#!/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 -eou pipefail +HELM_VERSION=${HELM_VERSION:-v3.17.3} + +mkdir -p ~/bin/ +NAMED_HELM=~/bin/helm-$HELM_VERSION + +if [[ ! -f $NAMED_HELM ]]; then + ARCH=$(uname -m) + case $ARCH in + x86_64) ARCH=amd64 ;; + aarch64) ARCH=arm64 ;; + *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; + esac + tmp_helm_dir=$(mktemp -d) + curl -sSL "https://get.helm.sh/helm-${HELM_VERSION}-linux-${ARCH}.tar.gz" | tar -xz -C "$tmp_helm_dir" --strip-components=1 + cp "$tmp_helm_dir/helm" "$NAMED_HELM" + rm -rf "$tmp_helm_dir" +fi + +echo "Installed helm at $NAMED_HELM" +echo "To use, you may set 'alias helm=$NAMED_HELM'" diff --git a/infra/kind/get-kubectl.sh b/infra/kind/get-kubectl.sh new file mode 100644 index 00000000000..c02d38e9959 --- /dev/null +++ b/infra/kind/get-kubectl.sh @@ -0,0 +1,71 @@ +#!/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 -eou pipefail + +KUBECTL_VERSION=${KUBECTL_VERSION:-latest} + +echo ============================================== +echo "Currently installed versions of kubectl:" +ls ~/bin/kubectl* 2>/dev/null || true +echo ============================================== + +mkdir -p ~/bin +NAMED_KUBECTL=~/bin/kubectl-$KUBECTL_VERSION + +if [[ ! -f $NAMED_KUBECTL ]]; then + ARCH=$(uname -m) + case $ARCH in + x86_64) ARCH=amd64 ;; + aarch64) ARCH=arm64 ;; + *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; + esac + if [[ $KUBECTL_VERSION == latest ]]; then + curl -L "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/$ARCH/kubectl" -o "$NAMED_KUBECTL" + else + curl -L "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/$ARCH/kubectl" -o "$NAMED_KUBECTL" + fi +fi +chmod +x "$NAMED_KUBECTL" + +echo "Installed kubectl at $NAMED_KUBECTL" +echo "To use, you may set 'alias kubectl=$NAMED_KUBECTL'" + +if [[ ! -f ~/.krew/bin/kubectl-krew ]]; then + ( + set -x; cd "$(mktemp -d)" && + OS="$(uname | tr '[:upper:]' '[:lower:]')" && + ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')" && + KREW="krew-${OS}_${ARCH}" && + curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/${KREW}.tar.gz" && + tar zxvf "${KREW}.tar.gz" && + ./"${KREW}" install krew + ) +else + echo "krew already installed" +fi + +$NAMED_KUBECTL krew install ctx +$NAMED_KUBECTL krew install ns +$NAMED_KUBECTL krew install stern +$NAMED_KUBECTL krew install view-allocations +$NAMED_KUBECTL krew install whoami + +cat <&2; exit 1 ;; + esac + curl -Lo "$NAMED_KIND" "https://kind.sigs.k8s.io/dl/$KIND_VERSION/kind-linux-$ARCH" + chmod +x "$NAMED_KIND" +fi +ln -sf "$NAMED_KIND" ~/bin/kind +echo "Installed kind $KIND_VERSION at $NAMED_KIND" + +# --- Install nvkind --- +if ! command -v nvkind &>/dev/null; then + GOBIN=~/bin go install github.com/NVIDIA/nvkind/cmd/nvkind@latest +fi +echo "Installed nvkind at $(which nvkind || echo ~/bin/nvkind)" diff --git a/infra/kind/nvkind-config-template.yaml b/infra/kind/nvkind-config-template.yaml new file mode 100644 index 00000000000..7e3f2a05b78 --- /dev/null +++ b/infra/kind/nvkind-config-template.yaml @@ -0,0 +1,52 @@ +# Custom nvkind config template with support for extraMounts. +# Based on the default nvkind template, extended to mount host directories +# into kind nodes (e.g., for local code development). +# +# Usage: +# nvkind cluster create \ +# --config-template nvkind-config-template.yaml \ +# --config-values nvkind-config-values-dev.yaml + +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +{{- if hasKey $ "name" }} +name: {{ $.name }} +{{- end }} +nodes: +- role: control-plane + {{- if hasKey $ "image" }} + image: {{ $.image }} + {{- end }} + {{- if hasKey $ "extraMounts" }} + extraMounts: + {{- range $.extraMounts }} + - hostPath: {{ .hostPath }} + containerPath: {{ .containerPath }} + {{- end }} + {{- end }} +{{- range $.workers }} +- role: worker + {{- if hasKey $ "image" }} + image: {{ $.image }} + {{- end }} + + {{- if hasKey . "devices" }} + {{- $devices := .devices }} + {{- if not (kindIs "slice" $devices) }} + {{- $devices = list .devices }} + {{- end }} + extraMounts: + # GPU device injection + {{- range $d := $devices }} + - hostPath: /dev/null + containerPath: /var/run/nvidia-container-devices/{{ $d }} + {{- end }} + # Additional host mounts + {{- if hasKey $ "extraMounts" }} + {{- range $.extraMounts }} + - hostPath: {{ .hostPath }} + containerPath: {{ .containerPath }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/infra/kind/nvkind-config-values-dev.yaml b/infra/kind/nvkind-config-values-dev.yaml new file mode 100644 index 00000000000..368264ea7d0 --- /dev/null +++ b/infra/kind/nvkind-config-values-dev.yaml @@ -0,0 +1,7 @@ +# Dev config: mount local nemo-rl source into kind nodes. +# Pods can then use hostPath: /workspace/nemo-rl to access the code. +workers: +- devices: all +extraMounts: +- hostPath: /home/terryk/nemo-rl + containerPath: /workspace/nemo-rl diff --git a/infra/kind/nvkind-config-values.yaml b/infra/kind/nvkind-config-values.yaml new file mode 100644 index 00000000000..f94f6475c4d --- /dev/null +++ b/infra/kind/nvkind-config-values.yaml @@ -0,0 +1,2 @@ +workers: +- devices: all From 73a5e1d32afadd9a83ce4dca772c364b0d292ea2 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 19:30:50 -0700 Subject: [PATCH 02/84] feat: NemoGym disaggregated mode with standalone Gym server (infra part) Signed-off-by: Terry Kong --- infra/examples/gym_standalone_config.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 infra/examples/gym_standalone_config.yaml diff --git a/infra/examples/gym_standalone_config.yaml b/infra/examples/gym_standalone_config.yaml new file mode 100644 index 00000000000..6cd45c7b544 --- /dev/null +++ b/infra/examples/gym_standalone_config.yaml @@ -0,0 +1,12 @@ +# Gym standalone server config (extracted from env.nemo_gym section). +# Used with: python -m nemo_gym.standalone_server --config-yaml this_file.yaml +config_paths: +- responses_api_models/vllm_model/configs/vllm_model_for_training.yaml +- resources_servers/workplace_assistant/configs/workplace_assistant.yaml +policy_model: + responses_api_models: + vllm_model: + extra_body: + chat_template_kwargs: + enable_thinking: false + uses_reasoning_parser: false From a8bd6c1b2624fbbb953e05e5a14496ce7197bf17 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 19:30:50 -0700 Subject: [PATCH 03/84] infra: K8s ConfigMap endpoint registry for disaggregated service discovery (infra part) Signed-off-by: Terry Kong --- infra/examples/disagg_gym_raycluster.yaml | 43 +++++++++++ infra/examples/disagg_rl_raycluster.yaml | 90 ++++++++++++++++++++++ infra/examples/endpoint-registry-rbac.yaml | 31 ++++++++ 3 files changed, 164 insertions(+) create mode 100644 infra/examples/disagg_gym_raycluster.yaml create mode 100644 infra/examples/disagg_rl_raycluster.yaml create mode 100644 infra/examples/endpoint-registry-rbac.yaml diff --git a/infra/examples/disagg_gym_raycluster.yaml b/infra/examples/disagg_gym_raycluster.yaml new file mode 100644 index 00000000000..50b30cf779f --- /dev/null +++ b/infra/examples/disagg_gym_raycluster.yaml @@ -0,0 +1,43 @@ +# Disaggregated Gym RayCluster. +# Runs NeMo Gym servers as a standalone HTTP service. +# Uses K8s ConfigMap endpoint registry for service discovery with the RL cluster. +# +# Prerequisites: +# kubectl apply -f endpoint-registry-rbac.yaml +# +# The Gym server registers its address and waits for the RL cluster to +# register vLLM URLs, all via the shared ConfigMap. +apiVersion: ray.io/v1 +kind: RayCluster +metadata: + name: raycluster-gym + labels: + kai.scheduler/queue: team-a +spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: + object-store-memory: "200000000" + template: + spec: + serviceAccountName: nemo-rl-endpoint-registry + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + containers: + - name: ray-head + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + resources: + limits: + cpu: "4" + memory: "16Gi" + requests: + cpu: "1" + memory: "4Gi" + ports: + - containerPort: 6379 + name: gcs-server + - containerPort: 8265 + name: dashboard + - containerPort: 8080 + name: head-server diff --git a/infra/examples/disagg_rl_raycluster.yaml b/infra/examples/disagg_rl_raycluster.yaml new file mode 100644 index 00000000000..ae132cde97e --- /dev/null +++ b/infra/examples/disagg_rl_raycluster.yaml @@ -0,0 +1,90 @@ +# Disaggregated RL RayCluster. +# Runs nemo-rl GRPO training with vLLM workers. +# Uses K8s ConfigMap endpoint registry for service discovery with the Gym cluster. +# +# Prerequisites: +# kubectl apply -f endpoint-registry-rbac.yaml +# +# Usage: +# kubectl apply -f disagg_rl_raycluster.yaml +# kubectl exec into head pod, then run GRPO with: +# env.disagg_job_id=my-job-123 +apiVersion: ray.io/v1 +kind: RayCluster +metadata: + name: raycluster-rl + labels: + kai.scheduler/queue: team-a +spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: + object-store-memory: "200000000" + template: + spec: + serviceAccountName: nemo-rl-endpoint-registry + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + containers: + - name: ray-head + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + env: + - name: RAY_CLUSTER_NAME + value: raycluster-rl + resources: + limits: + cpu: "4" + memory: "16Gi" + requests: + cpu: "1" + memory: "4Gi" + ports: + - containerPort: 6379 + name: gcs-server + - containerPort: 8265 + name: dashboard + - containerPort: 10001 + name: client + volumeMounts: + - name: nemo-rl-src + mountPath: /workspace/nemo-rl + volumes: + - name: nemo-rl-src + hostPath: + path: /workspace/nemo-rl + type: DirectoryOrCreate + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: + num-gpus: "2" + object-store-memory: "200000000" + template: + spec: + serviceAccountName: nemo-rl-endpoint-registry + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + containers: + - name: ray-worker + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + resources: + limits: + cpu: "8" + memory: "32Gi" + nvidia.com/gpu: "2" + requests: + cpu: "2" + memory: "8Gi" + nvidia.com/gpu: "2" + volumeMounts: + - name: nemo-rl-src + mountPath: /workspace/nemo-rl + volumes: + - name: nemo-rl-src + hostPath: + path: /workspace/nemo-rl + type: DirectoryOrCreate diff --git a/infra/examples/endpoint-registry-rbac.yaml b/infra/examples/endpoint-registry-rbac.yaml new file mode 100644 index 00000000000..b64378bd493 --- /dev/null +++ b/infra/examples/endpoint-registry-rbac.yaml @@ -0,0 +1,31 @@ +# RBAC for disaggregated RL-Gym service discovery. +# Allows Ray pods to CRUD ConfigMaps (as an endpoint registry) +# and read RayClusters (for ownerReference UID lookup). +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nemo-rl-endpoint-registry +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: nemo-rl-endpoint-registry +rules: +- apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "create", "update", "patch", "delete"] +- apiGroups: ["ray.io"] + resources: ["rayclusters"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: nemo-rl-endpoint-registry +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: nemo-rl-endpoint-registry +subjects: +- kind: ServiceAccount + name: nemo-rl-endpoint-registry From a25148e3464c02b57b3bfd361e3511f1929f5e94 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Mon, 30 Mar 2026 23:37:30 -0700 Subject: [PATCH 04/84] infra: peer-watcher sidecar for bidirectional failure cascading When RL and Gym run on separate RayClusters, either cluster failing or being deleted triggers teardown of both clusters to release resources. - peer-watcher.py: pure Python sidecar (no deps beyond stdlib), deployed as a ConfigMap volume mount on each head pod - Monitors peer RayCluster status via K8s API (polls every 10s) - Tears down after MAX_PEER_FAILURES (default 3) consecutive failures - Also monitors ConfigMap "error" key for application-level error signaling - Handles transient K8s API errors as failures (not false-healthy) - Added signal_error() to K8sEndpointRegistry - Updated disagg manifests with peer-watcher sidecar containers - Updated RBAC with "delete" verb for rayclusters Tested: deleting either cluster triggers teardown of both within ~10s. Signed-off-by: Terry Kong --- infra/examples/disagg_gym_raycluster.yaml | 40 +++++- infra/examples/disagg_rl_raycluster.yaml | 42 +++++- infra/examples/endpoint-registry-rbac.yaml | 2 +- infra/examples/peer-watcher.py | 160 +++++++++++++++++++++ 4 files changed, 232 insertions(+), 12 deletions(-) create mode 100644 infra/examples/peer-watcher.py diff --git a/infra/examples/disagg_gym_raycluster.yaml b/infra/examples/disagg_gym_raycluster.yaml index 50b30cf779f..2119e1703f2 100644 --- a/infra/examples/disagg_gym_raycluster.yaml +++ b/infra/examples/disagg_gym_raycluster.yaml @@ -1,12 +1,11 @@ -# Disaggregated Gym RayCluster. +# Disaggregated Gym RayCluster with peer-watcher sidecar. # Runs NeMo Gym servers as a standalone HTTP service. # Uses K8s ConfigMap endpoint registry for service discovery with the RL cluster. +# The peer-watcher sidecar monitors raycluster-rl and tears down both clusters +# if the RL cluster fails or the application signals an error. # # Prerequisites: # kubectl apply -f endpoint-registry-rbac.yaml -# -# The Gym server registers its address and waits for the RL cluster to -# register vLLM URLs, all via the shared ConfigMap. apiVersion: ray.io/v1 kind: RayCluster metadata: @@ -39,5 +38,36 @@ spec: name: gcs-server - containerPort: 8265 name: dashboard - - containerPort: 8080 + - containerPort: 9090 name: head-server + volumeMounts: + - name: peer-watcher-script + mountPath: /opt/peer-watcher + # Sidecar: monitors raycluster-rl, tears down both on failure. + - name: peer-watcher + image: python:3.12-slim + command: ["python3", "/opt/peer-watcher/peer-watcher.py"] + env: + - name: SELF_CLUSTER_NAME + value: raycluster-gym + - name: PEER_CLUSTER_NAME + value: raycluster-rl + - name: POLL_INTERVAL + value: "10" + - name: MAX_PEER_FAILURES + value: "3" + resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + cpu: "100m" + memory: "128Mi" + volumeMounts: + - name: peer-watcher-script + mountPath: /opt/peer-watcher + volumes: + - name: peer-watcher-script + configMap: + name: peer-watcher-script + defaultMode: 0755 diff --git a/infra/examples/disagg_rl_raycluster.yaml b/infra/examples/disagg_rl_raycluster.yaml index ae132cde97e..441f6c4757a 100644 --- a/infra/examples/disagg_rl_raycluster.yaml +++ b/infra/examples/disagg_rl_raycluster.yaml @@ -1,14 +1,11 @@ -# Disaggregated RL RayCluster. +# Disaggregated RL RayCluster with peer-watcher sidecar. # Runs nemo-rl GRPO training with vLLM workers. # Uses K8s ConfigMap endpoint registry for service discovery with the Gym cluster. +# The peer-watcher sidecar monitors raycluster-gym and tears down both clusters +# if the Gym cluster fails or the application signals an error. # # Prerequisites: # kubectl apply -f endpoint-registry-rbac.yaml -# -# Usage: -# kubectl apply -f disagg_rl_raycluster.yaml -# kubectl exec into head pod, then run GRPO with: -# env.disagg_job_id=my-job-123 apiVersion: ray.io/v1 kind: RayCluster metadata: @@ -49,11 +46,44 @@ spec: volumeMounts: - name: nemo-rl-src mountPath: /workspace/nemo-rl + - name: peer-watcher-script + mountPath: /opt/peer-watcher + # Sidecar: monitors raycluster-gym, tears down both on failure. + - name: peer-watcher + image: python:3.12-slim + command: ["python3", "/opt/peer-watcher/peer-watcher.py"] + env: + - name: SELF_CLUSTER_NAME + value: raycluster-rl + - name: PEER_CLUSTER_NAME + value: raycluster-gym + - name: POLL_INTERVAL + value: "10" + - name: MAX_PEER_FAILURES + value: "3" + # JOB_ID should be set to match +env.disagg_job_id in the GRPO command. + # Uncomment and set when using the endpoint registry. + # - name: JOB_ID + # value: "my-job-id" + resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + cpu: "100m" + memory: "128Mi" + volumeMounts: + - name: peer-watcher-script + mountPath: /opt/peer-watcher volumes: - name: nemo-rl-src hostPath: path: /workspace/nemo-rl type: DirectoryOrCreate + - name: peer-watcher-script + configMap: + name: peer-watcher-script + defaultMode: 0755 workerGroupSpecs: - groupName: gpu-workers replicas: 1 diff --git a/infra/examples/endpoint-registry-rbac.yaml b/infra/examples/endpoint-registry-rbac.yaml index b64378bd493..5e46c03b659 100644 --- a/infra/examples/endpoint-registry-rbac.yaml +++ b/infra/examples/endpoint-registry-rbac.yaml @@ -16,7 +16,7 @@ rules: verbs: ["get", "create", "update", "patch", "delete"] - apiGroups: ["ray.io"] resources: ["rayclusters"] - verbs: ["get"] + verbs: ["get", "delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding diff --git a/infra/examples/peer-watcher.py b/infra/examples/peer-watcher.py new file mode 100644 index 00000000000..97a2e7371ec --- /dev/null +++ b/infra/examples/peer-watcher.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# 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. +"""Sidecar that monitors a peer RayCluster and tears down both clusters on failure. + +Used for disaggregated RL-Gym setups where both clusters should fail fast together. +Runs as a sidecar container in the head pod of each RayCluster. + +Monitors: + 1. Peer RayCluster status (deleted / failed / suspended) + 2. ConfigMap "error" key (set by the application via K8sEndpointRegistry.signal_error()) + +Environment variables: + SELF_CLUSTER_NAME - Name of this RayCluster (to self-delete) + PEER_CLUSTER_NAME - Name of the peer RayCluster to watch + JOB_ID - Job ID for the ConfigMap endpoint registry (optional) + POLL_INTERVAL - Seconds between status checks (default: 10) + MAX_PEER_FAILURES - Consecutive failures before teardown (default: 3) +""" + +import json +import os +import ssl +import sys +import time +import urllib.request +from pathlib import Path + +SELF_CLUSTER_NAME = os.environ["SELF_CLUSTER_NAME"] +PEER_CLUSTER_NAME = os.environ["PEER_CLUSTER_NAME"] +JOB_ID = os.environ.get("JOB_ID", "") +POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL", "10")) +MAX_PEER_FAILURES = int(os.environ.get("MAX_PEER_FAILURES", "3")) + +SA_PATH = Path("/var/run/secrets/kubernetes.io/serviceaccount") +NAMESPACE = ( + (SA_PATH / "namespace").read_text().strip() + if (SA_PATH / "namespace").exists() + else "default" +) +TOKEN = (SA_PATH / "token").read_text().strip() if (SA_PATH / "token").exists() else "" +APISERVER = "https://kubernetes.default.svc" + +# Trust the in-cluster CA. +SSL_CTX = ( + ssl.create_default_context(cafile=str(SA_PATH / "ca.crt")) + if (SA_PATH / "ca.crt").exists() + else ssl._create_unverified_context() +) + + +def kube_request(path: str, method: str = "GET") -> dict: + req = urllib.request.Request(f"{APISERVER}{path}", method=method) + req.add_header("Authorization", f"Bearer {TOKEN}") + try: + with urllib.request.urlopen(req, context=SSL_CTX, timeout=10) as resp: + return json.loads(resp.read()) + except urllib.error.HTTPError as e: + return {"code": e.code, "message": e.reason} + except Exception as e: + return {"code": 0, "message": str(e)} + + +def teardown(reason: str): + print(f"[peer-watcher] TEARING DOWN: {reason}", flush=True) + path_prefix = f"/apis/ray.io/v1/namespaces/{NAMESPACE}/rayclusters" + for name in (SELF_CLUSTER_NAME, PEER_CLUSTER_NAME): + print(f"[peer-watcher] Deleting RayCluster: {name}", flush=True) + kube_request(f"{path_prefix}/{name}", method="DELETE") + if JOB_ID: + print( + f"[peer-watcher] Deleting ConfigMap: nemo-rl-endpoints-{JOB_ID}", flush=True + ) + kube_request( + f"/api/v1/namespaces/{NAMESPACE}/configmaps/nemo-rl-endpoints-{JOB_ID}", + method="DELETE", + ) + sys.exit(1) + + +def main(): + print( + f"[peer-watcher] Watching peer={PEER_CLUSTER_NAME}, self={SELF_CLUSTER_NAME}", + flush=True, + ) + print( + f"[peer-watcher] namespace={NAMESPACE}, poll={POLL_INTERVAL}s, max_failures={MAX_PEER_FAILURES}", + flush=True, + ) + + consecutive_failures = 0 + + while True: + time.sleep(POLL_INTERVAL) + + # Check peer RayCluster. + resp = kube_request( + f"/apis/ray.io/v1/namespaces/{NAMESPACE}/rayclusters/{PEER_CLUSTER_NAME}" + ) + code = resp.get("code", 0) + if code == 404: + teardown(f"Peer {PEER_CLUSTER_NAME} not found (deleted)") + + status = resp.get("status", {}).get("state", "") + if status in ("failed", "suspended"): + consecutive_failures += 1 + print( + f"[peer-watcher] Peer {PEER_CLUSTER_NAME} is {status} ({consecutive_failures}/{MAX_PEER_FAILURES})", + flush=True, + ) + if consecutive_failures >= MAX_PEER_FAILURES: + teardown( + f"Peer {PEER_CLUSTER_NAME} failed {MAX_PEER_FAILURES} consecutive checks" + ) + continue + + # Check ConfigMap error signal. + if JOB_ID: + cm = kube_request( + f"/api/v1/namespaces/{NAMESPACE}/configmaps/nemo-rl-endpoints-{JOB_ID}" + ) + error = cm.get("data", {}).get("error", "") + if error: + teardown(f"Error signaled via ConfigMap: {error}") + + # Unknown state (e.g., K8s API error, transient network issue) — treat as failure. + if code != 0 and status == "": + consecutive_failures += 1 + print( + f"[peer-watcher] Could not determine peer status (code={code}), treating as failure ({consecutive_failures}/{MAX_PEER_FAILURES})", + flush=True, + ) + if consecutive_failures >= MAX_PEER_FAILURES: + teardown( + f"Peer {PEER_CLUSTER_NAME} unreachable for {MAX_PEER_FAILURES} consecutive checks" + ) + continue + + # Peer healthy. + if consecutive_failures > 0: + print( + f"[peer-watcher] Peer {PEER_CLUSTER_NAME} recovered (status={status})", + flush=True, + ) + consecutive_failures = 0 + + +if __name__ == "__main__": + main() From 63129bdd974b4529fb9d755d3d5884d8e538d3c8 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Tue, 31 Mar 2026 01:21:24 -0700 Subject: [PATCH 05/84] infra: Kyverno queue enforcement, Prometheus+Grafana monitoring, fairshare configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Kyverno policy: RayCluster/RayJob must have kai.scheduler/queue label. Validates at CRD level (not pod) since KubeRay operator creates pods. Optional Policy 2 for user→queue access control via ConfigMap. - kube-prometheus-stack: Prometheus + Grafana for fairshare monitoring. Pre-built Grafana dashboard showing GPU allocation vs fair share, preemption events, and scheduling latency per queue. - ServiceMonitors for KAI scheduler, binder, and queue-controller. - Example queue configs: - kai-queue.yaml: 2-GPU kind cluster (2 teams, equal quotas) - kai-queue-prod.yaml: 256-GPU prod (3 departments, 6 teams) - preemptMinRuntime: 4h (protect long training runs from priority preemption) - reclaimMinRuntime: 15m (fast fairness reclaim of over-quota resources) - SETUP.md: fairshare docs, preempt vs reclaim explanation, Grafana access. Tested: Kyverno rejects RayCluster without queue label, accepts with. Team A 2-GPU job reclaimed when Team B submitted to its guaranteed quota. Signed-off-by: Terry Kong --- infra/examples/disagg_gym_raycluster.yaml | 2 +- infra/examples/disagg_rl_raycluster.yaml | 2 +- infra/examples/kai-grafana-dashboard.yaml | 122 ++++++++++++++++ infra/examples/kai-queue-prod.yaml | 131 ++++++++++++++++++ infra/examples/kai-queue.yaml | 75 ++++++---- infra/examples/kai-service-monitors.yaml | 53 +++++++ infra/examples/kai_scheduled_pods.yaml | 6 +- infra/examples/kai_scheduled_rayclusters.yaml | 4 +- infra/examples/kyverno-kai-policies.yaml | 106 ++++++++++++++ infra/examples/sft_rayjob.yaml | 2 +- infra/helm/helmfile.yaml | 32 ++++- infra/helm/values/kai-scheduler.yaml | 8 ++ infra/helm/values/kube-prometheus-stack.yaml | 46 ++++++ infra/helm/values/kyverno.yaml | 10 ++ infra/kind/SETUP.md | 53 ++++++- 15 files changed, 613 insertions(+), 39 deletions(-) create mode 100644 infra/examples/kai-grafana-dashboard.yaml create mode 100644 infra/examples/kai-queue-prod.yaml create mode 100644 infra/examples/kai-service-monitors.yaml create mode 100644 infra/examples/kyverno-kai-policies.yaml create mode 100644 infra/helm/values/kube-prometheus-stack.yaml create mode 100644 infra/helm/values/kyverno.yaml diff --git a/infra/examples/disagg_gym_raycluster.yaml b/infra/examples/disagg_gym_raycluster.yaml index 2119e1703f2..eaa951ff482 100644 --- a/infra/examples/disagg_gym_raycluster.yaml +++ b/infra/examples/disagg_gym_raycluster.yaml @@ -11,7 +11,7 @@ kind: RayCluster metadata: name: raycluster-gym labels: - kai.scheduler/queue: team-a + kai.scheduler/queue: priority-team spec: rayVersion: "2.52.0" headGroupSpec: diff --git a/infra/examples/disagg_rl_raycluster.yaml b/infra/examples/disagg_rl_raycluster.yaml index 441f6c4757a..b4b30cf8622 100644 --- a/infra/examples/disagg_rl_raycluster.yaml +++ b/infra/examples/disagg_rl_raycluster.yaml @@ -11,7 +11,7 @@ kind: RayCluster metadata: name: raycluster-rl labels: - kai.scheduler/queue: team-a + kai.scheduler/queue: priority-team spec: rayVersion: "2.52.0" headGroupSpec: diff --git a/infra/examples/kai-grafana-dashboard.yaml b/infra/examples/kai-grafana-dashboard.yaml new file mode 100644 index 00000000000..d6bad06fb0c --- /dev/null +++ b/infra/examples/kai-grafana-dashboard.yaml @@ -0,0 +1,122 @@ +# Grafana dashboard for KAI scheduler fairshare monitoring. +# Deployed as a ConfigMap — Grafana's sidecar auto-discovers it. +# +# Panels: +# 1. GPU Allocation vs Fair Share per Queue (bar chart) +# 2. GPU Allocation Over Time (time series) +# 3. Preemption Events (counter) +# 4. Scheduling Latency (gauge) +apiVersion: v1 +kind: ConfigMap +metadata: + name: kai-fairshare-dashboard + namespace: monitoring + labels: + grafana_dashboard: "1" +data: + kai-fairshare.json: | + { + "annotations": { "list": [] }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "title": "GPU Allocation vs Fair Share per Queue", + "description": "Current GPU allocation (solid) vs computed fair share (dashed) for each queue. Queues above their fair share may have resources reclaimed.", + "type": "bargauge", + "gridPos": { "h": 8, "w": 24, "x": 0, "y": 0 }, + "targets": [ + { + "expr": "queue_allocated_gpus", + "legendFormat": "{{queue_name}} allocated", + "refId": "A" + }, + { + "expr": "queue_fair_share_gpu", + "legendFormat": "{{queue_name}} fair share", + "refId": "B" + }, + { + "expr": "queue_deserved_gpus", + "legendFormat": "{{queue_name}} deserved (quota)", + "refId": "C" + } + ], + "fieldConfig": { + "defaults": { + "unit": "short", + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } + } + } + }, + { + "title": "GPU Allocation Over Time", + "description": "How GPU allocation per queue changes over time. Useful for observing fairshare oscillation and reclaim events.", + "type": "timeseries", + "gridPos": { "h": 10, "w": 24, "x": 0, "y": 8 }, + "targets": [ + { + "expr": "queue_allocated_gpus", + "legendFormat": "{{queue_name}} allocated", + "refId": "A" + }, + { + "expr": "queue_fair_share_gpu", + "legendFormat": "{{queue_name}} fair share", + "refId": "B" + } + ], + "fieldConfig": { + "defaults": { + "unit": "short", + "custom": { "lineWidth": 2, "fillOpacity": 10 } + } + } + }, + { + "title": "Preemption & Eviction Events", + "description": "Count of preemption attempts and pod evictions. Spikes indicate resource contention between queues.", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 18 }, + "targets": [ + { + "expr": "rate(total_preemption_attempts[5m])", + "legendFormat": "preemption attempts/s", + "refId": "A" + }, + { + "expr": "rate(pod_group_evicted_pods_total[5m])", + "legendFormat": "evictions/s ({{queue_name}})", + "refId": "B" + } + ], + "fieldConfig": { + "defaults": { "unit": "ops", "custom": { "lineWidth": 2 } } + } + }, + { + "title": "Scheduling Latency", + "description": "End-to-end scheduling cycle latency. High latency may indicate resource fragmentation or too many pending workloads.", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 18 }, + "targets": [ + { + "expr": "e2e_scheduling_latency_milliseconds", + "legendFormat": "e2e latency (ms)", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { "unit": "ms", "custom": { "lineWidth": 2 } } + } + } + ], + "schemaVersion": 39, + "tags": ["kai", "fairshare", "gpu"], + "templating": { "list": [] }, + "time": { "from": "now-1h", "to": "now" }, + "title": "KAI Scheduler Fairshare", + "uid": "kai-fairshare" + } diff --git a/infra/examples/kai-queue-prod.yaml b/infra/examples/kai-queue-prod.yaml new file mode 100644 index 00000000000..1b34306502f --- /dev/null +++ b/infra/examples/kai-queue-prod.yaml @@ -0,0 +1,131 @@ +# KAI Scheduler queue hierarchy for a 256-GPU production cluster. +# Imbalanced setup: priority teams get guarantees and preferential treatment, +# community teams run best-effort on idle resources. +# +# Queue hierarchy: +# org (root, unlimited) +# ├── priority (department, priority 200, 192 GPU quota, burst to 240) +# │ ├── priority-training (128 GPU quota, burst to 192, weight 2) +# │ └── priority-inference (64 GPU quota, burst to 80, weight 3) +# └── community (department, priority 50, 32 GPU quota, burst to 128) +# ├── community-team-a (16 GPU quota, burst to 64) +# └── community-team-b (16 GPU quota, burst to 64) +# +# Total guaranteed: 192 + 32 = 224 GPUs (out of 256). +# Remaining 32 GPUs go to priority department first (priority 200 > 50). +# +# preemptMinRuntime: "4h" — protects running jobs from higher-priority preemption +# for at least 4 hours. Prevents expensive training runs from being killed early. +# reclaimMinRuntime: "15m" — allows fast reclaim of over-quota resources. +# When a team is using more than its fair share and another team needs its +# guaranteed quota, reclaim happens after a 15-minute grace period. +# This is shorter than preempt because reclaim is about fairness (giving back +# what you owe), not priority (a VIP taking your resources). + +# ---------- Root ---------- +apiVersion: scheduling.run.ai/v2 +kind: Queue +metadata: + name: org +spec: + resources: + gpu: { quota: -1, limit: -1, overQuotaWeight: 1 } + cpu: { quota: -1, limit: -1, overQuotaWeight: 1 } + memory: { quota: -1, limit: -1, overQuotaWeight: 1 } + +--- +# ---------- Priority department ---------- +# VIP teams with guaranteed resources and highest over-quota priority. +# Gets surplus GPUs before community, reclaimed last. +apiVersion: scheduling.run.ai/v2 +kind: Queue +metadata: + name: priority +spec: + parentQueue: org + priority: 200 # served first for over-quota, reclaimed last + resources: + gpu: { quota: 192, limit: 240, overQuotaWeight: 3 } + cpu: { quota: -1, limit: -1, overQuotaWeight: 3 } + memory: { quota: -1, limit: -1, overQuotaWeight: 3 } + +--- +# Priority training — large distributed training jobs. +# 128 GPUs guaranteed, burst to 192 for big runs. +# Weight 2 within priority department (gets 2/5 of priority's surplus vs inference's 3/5). +apiVersion: scheduling.run.ai/v2 +kind: Queue +metadata: + name: priority-training +spec: + parentQueue: priority + preemptMinRuntime: "4h" + reclaimMinRuntime: "15m" + resources: + gpu: { quota: 128, limit: 192, overQuotaWeight: 2 } + cpu: { quota: -1, limit: -1, overQuotaWeight: 2 } + memory: { quota: -1, limit: -1, overQuotaWeight: 2 } + +--- +# Priority inference — latency-sensitive serving. +# 64 GPUs guaranteed, capped at 80 (don't over-provision serving). +# Weight 3 within priority department (gets 3/5 of priority's surplus). +apiVersion: scheduling.run.ai/v2 +kind: Queue +metadata: + name: priority-inference +spec: + parentQueue: priority + preemptMinRuntime: "4h" + reclaimMinRuntime: "15m" + resources: + gpu: { quota: 64, limit: 80, overQuotaWeight: 3 } + cpu: { quota: -1, limit: -1, overQuotaWeight: 3 } + memory: { quota: -1, limit: -1, overQuotaWeight: 3 } + +--- +# ---------- Community department ---------- +# Best-effort teams that use idle resources. Low priority, small guarantees. +# First to be reclaimed when priority department needs resources. +apiVersion: scheduling.run.ai/v2 +kind: Queue +metadata: + name: community +spec: + parentQueue: org + priority: 50 # lowest priority — served last, reclaimed first + resources: + gpu: { quota: 32, limit: 128, overQuotaWeight: 1 } + cpu: { quota: -1, limit: -1, overQuotaWeight: 1 } + memory: { quota: -1, limit: -1, overQuotaWeight: 1 } + +--- +# Community Team A — equal split of community's small guarantee. +# Can burst to 64 GPUs when the cluster is idle. +apiVersion: scheduling.run.ai/v2 +kind: Queue +metadata: + name: community-team-a +spec: + parentQueue: community + preemptMinRuntime: "4h" + reclaimMinRuntime: "15m" + resources: + gpu: { quota: 16, limit: 64, overQuotaWeight: 1 } + cpu: { quota: -1, limit: -1, overQuotaWeight: 1 } + memory: { quota: -1, limit: -1, overQuotaWeight: 1 } + +--- +# Community Team B — same as Team A. +apiVersion: scheduling.run.ai/v2 +kind: Queue +metadata: + name: community-team-b +spec: + parentQueue: community + preemptMinRuntime: "4h" + reclaimMinRuntime: "15m" + resources: + gpu: { quota: 16, limit: 64, overQuotaWeight: 1 } + cpu: { quota: -1, limit: -1, overQuotaWeight: 1 } + memory: { quota: -1, limit: -1, overQuotaWeight: 1 } diff --git a/infra/examples/kai-queue.yaml b/infra/examples/kai-queue.yaml index 1ca9912ea19..1e8e4f17643 100644 --- a/infra/examples/kai-queue.yaml +++ b/infra/examples/kai-queue.yaml @@ -1,40 +1,61 @@ -# KAI Scheduler queue hierarchy for local dev. -# Unlimited quotas — appropriate for single-user kind clusters. +# KAI Scheduler queue hierarchy for a 2-GPU kind cluster. +# One priority team and one best-effort team to demonstrate fairshare imbalance. +# +# Queue hierarchy: +# org (root, unlimited) +# ├── priority-team (1 GPU guaranteed, burst to 2, higher priority + weight) +# └── community (0 GPU guaranteed, uses idle GPUs only, lower priority) +# +# Behavior: +# - priority-team always gets its 1 GPU quota, and gets surplus first (priority 200). +# - community has no guarantee — it runs on whatever priority-team isn't using. +# - If both compete, priority-team gets 2x the surplus (overQuotaWeight 2 vs 1). +# - community jobs can be reclaimed after 15m if priority-team needs resources back. +# - priority-team jobs are protected from preemption for 4 hours. + apiVersion: scheduling.run.ai/v2 kind: Queue metadata: - name: department-1 + name: org spec: + resources: + gpu: { quota: -1, limit: -1, overQuotaWeight: 1 } + cpu: { quota: -1, limit: -1, overQuotaWeight: 1 } + memory: { quota: -1, limit: -1, overQuotaWeight: 1 } +--- +# Priority team — guaranteed resources, high priority, protected from preemption. +apiVersion: scheduling.run.ai/v2 +kind: Queue +metadata: + name: priority-team +spec: + parentQueue: org + priority: 200 # served first for over-quota, reclaimed last + preemptMinRuntime: "4h" # 4 hours before a higher-priority queue can preempt + reclaimMinRuntime: "15m" # 15 min grace before over-quota resources reclaimed resources: gpu: - quota: -1 - limit: -1 - overQuotaWeight: 1 - cpu: - quota: -1 - limit: -1 - overQuotaWeight: 1 - memory: - quota: -1 - limit: -1 - overQuotaWeight: 1 + quota: 1 # guaranteed 1 GPU + limit: 2 # can burst to full cluster when idle + overQuotaWeight: 2 # gets 2x surplus share vs community + cpu: { quota: -1, limit: -1, overQuotaWeight: 2 } + memory: { quota: -1, limit: -1, overQuotaWeight: 2 } --- +# Community — best-effort, no guarantee, uses idle resources. +# First to be reclaimed when priority-team needs resources back. apiVersion: scheduling.run.ai/v2 kind: Queue metadata: - name: team-a + name: community spec: - parentQueue: department-1 + parentQueue: org + priority: 50 # lowest priority — served last, reclaimed first + preemptMinRuntime: "4h" # still protect running jobs for 4 hours + reclaimMinRuntime: "15m" # 15 min grace (same as priority-team for fairness) resources: gpu: - quota: -1 - limit: -1 - overQuotaWeight: 1 - cpu: - quota: -1 - limit: -1 - overQuotaWeight: 1 - memory: - quota: -1 - limit: -1 - overQuotaWeight: 1 + quota: 0 # no guaranteed GPUs + limit: 2 # can use the whole cluster if nobody else needs it + overQuotaWeight: 1 # half the surplus weight of priority-team + cpu: { quota: -1, limit: -1, overQuotaWeight: 1 } + memory: { quota: -1, limit: -1, overQuotaWeight: 1 } diff --git a/infra/examples/kai-service-monitors.yaml b/infra/examples/kai-service-monitors.yaml new file mode 100644 index 00000000000..3ee42c8fab9 --- /dev/null +++ b/infra/examples/kai-service-monitors.yaml @@ -0,0 +1,53 @@ +# ServiceMonitors for KAI scheduler components. +# These tell Prometheus to scrape metrics from the scheduler, binder, and queue-controller. +# +# Prerequisites: kube-prometheus-stack must be installed (provides Prometheus Operator). +# Apply after: helmfile sync + +# Scheduler metrics: scheduling latency, fairshare allocation, preemption counts. +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: kai-scheduler + namespace: kai-scheduler + labels: + app: kai-scheduler +spec: + selector: + matchLabels: + app: kai-scheduler-default + endpoints: + - port: metrics + interval: 15s +--- +# Binder metrics: bind latency, bind success/failure rates. +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: kai-binder + namespace: kai-scheduler + labels: + app: kai-binder +spec: + selector: + matchLabels: + app: binder + endpoints: + - port: metrics + interval: 15s +--- +# Queue controller metrics: queue allocation, deserved GPUs, fair share. +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: kai-queue-controller + namespace: kai-scheduler + labels: + app: kai-queue-controller +spec: + selector: + matchLabels: + app: queue-controller + endpoints: + - port: metrics + interval: 15s diff --git a/infra/examples/kai_scheduled_pods.yaml b/infra/examples/kai_scheduled_pods.yaml index 94a7a836353..aeceda565cc 100644 --- a/infra/examples/kai_scheduled_pods.yaml +++ b/infra/examples/kai_scheduled_pods.yaml @@ -6,14 +6,14 @@ metadata: name: gpu-test-group spec: minMember: 2 - queue: team-a + queue: priority-team --- apiVersion: v1 kind: Pod metadata: name: gpu-test-0 labels: - kai.scheduler/queue: team-a + kai.scheduler/queue: priority-team annotations: pod-group-name: gpu-test-group spec: @@ -32,7 +32,7 @@ kind: Pod metadata: name: gpu-test-1 labels: - kai.scheduler/queue: team-a + kai.scheduler/queue: priority-team annotations: pod-group-name: gpu-test-group spec: diff --git a/infra/examples/kai_scheduled_rayclusters.yaml b/infra/examples/kai_scheduled_rayclusters.yaml index 2467f918284..2e91a189566 100644 --- a/infra/examples/kai_scheduled_rayclusters.yaml +++ b/infra/examples/kai_scheduled_rayclusters.yaml @@ -5,7 +5,7 @@ kind: RayCluster metadata: name: raycluster-a labels: - kai.scheduler/queue: team-a + kai.scheduler/queue: priority-team spec: rayVersion: "2.52.0" headGroupSpec: @@ -60,7 +60,7 @@ kind: RayCluster metadata: name: raycluster-b labels: - kai.scheduler/queue: team-a + kai.scheduler/queue: priority-team spec: rayVersion: "2.52.0" headGroupSpec: diff --git a/infra/examples/kyverno-kai-policies.yaml b/infra/examples/kyverno-kai-policies.yaml new file mode 100644 index 00000000000..f500edc2922 --- /dev/null +++ b/infra/examples/kyverno-kai-policies.yaml @@ -0,0 +1,106 @@ +# Kyverno policies for KAI scheduler queue enforcement. +# +# Policy 1: RayCluster and RayJob must have a kai.scheduler/queue label. +# Validates at the CRD level (not Pod level) because the KubeRay operator +# creates pods — pod-level validation would check the operator's identity, +# not the actual user who submitted the workload. +# +# Policy 2 (optional): Validate that the queue is allowed for the requesting user. +# Uses a ConfigMap (kai-queue-permissions) for user→queue mapping. +# The user identity comes from the K8s AdmissionReview userInfo (OIDC email, +# certificate CN, or ServiceAccount name). +# +# Prerequisites: +# helm install kyverno kyverno/kyverno -n kyverno --create-namespace + +# --- Policy 1: Require queue label --- +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-kai-queue-label + annotations: + policies.kyverno.io/title: Require KAI queue label on Ray workloads + policies.kyverno.io/description: >- + RayCluster and RayJob resources must specify a kai.scheduler/queue label + so the KAI scheduler can assign them to the correct fairshare queue. +spec: + validationFailureAction: Enforce + background: true + rules: + - name: require-kai-queue-on-raycluster + match: + any: + - resources: + kinds: + - ray.io/v1/RayCluster + validate: + message: >- + RayCluster "{{request.object.metadata.name}}" must have a + 'kai.scheduler/queue' label. Add it to metadata.labels. + pattern: + metadata: + labels: + kai.scheduler/queue: "?*" + - name: require-kai-queue-on-rayjob + match: + any: + - resources: + kinds: + - ray.io/v1/RayJob + validate: + message: >- + RayJob "{{request.object.metadata.name}}" must have a + 'kai.scheduler/queue' label. Add it to metadata.labels. + pattern: + metadata: + labels: + kai.scheduler/queue: "?*" + +--- +# --- Policy 2: Validate user is allowed to use the queue (optional) --- +# Uncomment and configure the ConfigMap below to enable. +# The ConfigMap maps K8s usernames to comma-separated allowed queues. +# +# apiVersion: v1 +# kind: ConfigMap +# metadata: +# name: kai-queue-permissions +# namespace: kyverno +# data: +# # K8s username → allowed queues (comma-separated) +# alice@company.com: "team-a,team-b" +# bob@company.com: "team-b" +# # ServiceAccounts use the format: system:serviceaccount:namespace:name +# system:serviceaccount:default:nemo-rl-endpoint-registry: "team-a,team-b" +# --- +# apiVersion: kyverno.io/v1 +# kind: ClusterPolicy +# metadata: +# name: validate-kai-queue-permission +# spec: +# validationFailureAction: Enforce +# background: false +# rules: +# - name: check-queue-permission +# match: +# any: +# - resources: +# kinds: +# - ray.io/v1/RayCluster +# - ray.io/v1/RayJob +# context: +# - name: permissions +# configMap: +# name: kai-queue-permissions +# namespace: kyverno +# validate: +# message: >- +# User "{{request.userInfo.username}}" is not allowed to use queue +# "{{request.object.metadata.labels."kai.scheduler/queue"}}". +# Allowed queues: {{permissions.data[request.userInfo.username] || 'none'}} +# deny: +# conditions: +# all: +# - key: "{{request.object.metadata.labels.\"kai.scheduler/queue\"}}" +# operator: AnyNotIn +# value: "{{permissions.data[request.userInfo.username] || '' | split(@, ',')}}" diff --git a/infra/examples/sft_rayjob.yaml b/infra/examples/sft_rayjob.yaml index 8424b56b0dd..030f422fcab 100644 --- a/infra/examples/sft_rayjob.yaml +++ b/infra/examples/sft_rayjob.yaml @@ -5,7 +5,7 @@ kind: RayJob metadata: name: sft-job labels: - kai.scheduler/queue: team-a + kai.scheduler/queue: priority-team spec: entrypoint: "cd /opt/nemo-rl && bash tests/functional/sft.sh" # HTTPMode is required with KAI scheduler — K8sJobMode creates a separate diff --git a/infra/helm/helmfile.yaml b/infra/helm/helmfile.yaml index 597b6eadf09..f867272cb3a 100644 --- a/infra/helm/helmfile.yaml +++ b/infra/helm/helmfile.yaml @@ -17,6 +17,8 @@ repositories: url: https://ray-project.github.io/kuberay-helm/ - name: prometheus-community url: https://prometheus-community.github.io/helm-charts +- name: kyverno + url: https://kyverno.github.io/kyverno/ releases: # @@ -47,7 +49,7 @@ releases: {{ end }} # -# KAI Scheduler: gang scheduling for GPU workloads +# KAI Scheduler: gang scheduling + fairshare for GPU workloads # - name: kai-scheduler namespace: kai-scheduler @@ -58,7 +60,7 @@ releases: - values/kai-scheduler.yaml # -# KubeRay Operator: manages RayCluster CRDs, integrated with KAI for gang scheduling +# KubeRay Operator: manages RayCluster/RayJob CRDs, integrated with KAI # - name: kuberay-operator namespace: kuberay-system @@ -68,3 +70,29 @@ releases: wait: true values: - values/kuberay-operator.yaml + +# +# Kyverno: policy engine for enforcing queue labels on Ray workloads. +# Policies are applied separately: kubectl apply -f kyverno-kai-policies.yaml +# +- name: kyverno + namespace: kyverno + createNamespace: true + chart: kyverno/kyverno + version: 3.7.1 + wait: true + values: + - values/kyverno.yaml + +# +# Prometheus + Grafana: fairshare monitoring and dashboards. +# Access Grafana: kubectl port-forward svc/kube-prometheus-stack-grafana -n monitoring 3000:80 +# +- name: kube-prometheus-stack + namespace: monitoring + createNamespace: true + chart: prometheus-community/kube-prometheus-stack + wait: true + waitForJobs: true + values: + - values/kube-prometheus-stack.yaml diff --git a/infra/helm/values/kai-scheduler.yaml b/infra/helm/values/kai-scheduler.yaml index 0bdd979ae9d..3e50318b6ca 100644 --- a/infra/helm/values/kai-scheduler.yaml +++ b/infra/helm/values/kai-scheduler.yaml @@ -1,2 +1,10 @@ +# KAI Scheduler configuration. +# Creates a default queue and enables Prometheus metrics for fairshare monitoring. defaultQueue: createDefaultQueue: true + +# Enable Prometheus metrics endpoint on KAI components. +# Metrics are scraped by the kube-prometheus-stack ServiceMonitors. +global: + prometheus: + enabled: true diff --git a/infra/helm/values/kube-prometheus-stack.yaml b/infra/helm/values/kube-prometheus-stack.yaml new file mode 100644 index 00000000000..6e05689900d --- /dev/null +++ b/infra/helm/values/kube-prometheus-stack.yaml @@ -0,0 +1,46 @@ +# Prometheus + Grafana for KAI scheduler fairshare monitoring. +# +# Access Grafana: kubectl port-forward svc/kube-prometheus-stack-grafana -n monitoring 3000:80 +# Default credentials: admin / prom-operator + +prometheus: + prometheusSpec: + # Scrape KAI metrics from the kai-scheduler namespace. + serviceMonitorSelectorNilUsesHelmValues: false + serviceMonitorNamespaceSelector: {} + serviceMonitorSelector: {} + # Reduce resource usage for kind dev cluster. + resources: + requests: + cpu: 200m + memory: 512Mi + limits: + memory: 2Gi + retention: 7d + storageSpec: + volumeClaimTemplate: + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 5Gi + +grafana: + enabled: true + adminPassword: prom-operator + persistence: + enabled: false + sidecar: + dashboards: + enabled: true # auto-discovers ConfigMaps with grafana_dashboard: "1" + searchNamespace: ALL + datasources: + enabled: true + +# Disable components we don't need on a kind dev cluster. +alertmanager: + enabled: false +nodeExporter: + enabled: false +kubeStateMetrics: + enabled: false diff --git a/infra/helm/values/kyverno.yaml b/infra/helm/values/kyverno.yaml new file mode 100644 index 00000000000..3c9f7d5099a --- /dev/null +++ b/infra/helm/values/kyverno.yaml @@ -0,0 +1,10 @@ +# Kyverno policy engine — enforces queue labels on Ray workloads. +# Policies are applied separately via: kubectl apply -f kyverno-kai-policies.yaml +admissionController: + replicas: 1 +backgroundController: + replicas: 1 +cleanupController: + replicas: 1 +reportsController: + replicas: 1 diff --git a/infra/kind/SETUP.md b/infra/kind/SETUP.md index 9fb97542931..3f7dff8e6d6 100644 --- a/infra/kind/SETUP.md +++ b/infra/kind/SETUP.md @@ -97,7 +97,9 @@ infra/ │ ├── nvidia-device-plugin.yaml # kind only │ ├── gpu-operator.yaml # prod only │ ├── kai-scheduler.yaml -│ └── kuberay-operator.yaml +│ ├── kuberay-operator.yaml +│ ├── kyverno.yaml # Kyverno policy engine +│ └── kube-prometheus-stack.yaml # Prometheus + Grafana └── examples/ ├── kai-queue.yaml # KAI queue hierarchy ├── kai_scheduled_pods.yaml # Gang-scheduled GPU test pods @@ -109,7 +111,11 @@ infra/ ├── disagg_rl_raycluster.yaml # Disagg RL cluster + peer-watcher ├── disagg_gym_raycluster.yaml # Disagg Gym cluster + peer-watcher ├── endpoint-registry-rbac.yaml # RBAC for ConfigMap endpoint registry - └── peer-watcher.py # Sidecar for failure cascading + ├── peer-watcher.py # Sidecar for failure cascading + ├── kai-queue-prod.yaml # 256-GPU prod queue config + ├── kyverno-kai-policies.yaml # Queue enforcement policies + ├── kai-service-monitors.yaml # Prometheus ServiceMonitors for KAI + └── kai-grafana-dashboard.yaml # Grafana dashboard for fairshare ``` ## Notes @@ -135,6 +141,49 @@ kubectl create configmap peer-watcher-script --from-file=peer-watcher.py=infra/e kubectl apply -f infra/examples/endpoint-registry-rbac.yaml ``` +## Fairshare scheduling + +KAI distributes GPU resources using hierarchical fair-share with two phases: +1. **Guaranteed quota**: Each queue gets its `quota` first, unconditionally. +2. **Over-quota surplus**: Remaining GPUs distributed by `priority` (higher served first), then `overQuotaWeight` within the same priority level. + +### Queue fields + +| Field | Description | +|-------|-------------| +| `quota` | Guaranteed GPUs. `-1` = unlimited, `0` = no guarantee | +| `limit` | Hard cap on total GPUs. `-1` = no limit | +| `overQuotaWeight` | Weight for surplus distribution (higher = bigger share) | +| `priority` | Over-quota allocation order (higher = served first, reclaimed last) | +| `preemptMinRuntime` | Min runtime before a higher-priority queue can preempt (default: `"4h"`) | +| `reclaimMinRuntime` | Min runtime before over-quota resources can be reclaimed (default: `"15m"`) | + +### Preempt vs reclaim + +- **Preempt**: A higher-priority queue takes from a lower-priority queue. (VIP takes your table.) +- **Reclaim**: A queue takes back what it's entitled to from an over-allocated queue. (Fairness — give back what you owe.) + +`reclaimMinRuntime` is shorter than `preemptMinRuntime` because reclaim is about fairness (returning over-quota resources quickly), while preempt protects long-running jobs from priority-based interruption. + +### Example configs + +- `kai-queue.yaml` — 2-GPU kind cluster (team-a, team-b, equal quotas) +- `kai-queue-prod.yaml` — 256-GPU production cluster (3 departments, 6 teams) + +### Monitoring (Grafana) + +```sh +kubectl port-forward svc/kube-prometheus-stack-grafana -n monitoring 3000:80 +# Login: admin / prom-operator +# Dashboard: "KAI Scheduler Fairshare" +``` + +Key metrics: `queue_allocated_gpus`, `queue_fair_share_gpu`, `queue_deserved_gpus`, `total_preemption_attempts`. + +### Kyverno queue enforcement + +RayCluster and RayJob resources must have a `kai.scheduler/queue` label or they're rejected by Kyverno. To enable user→queue access control, uncomment Policy 2 in `kyverno-kai-policies.yaml` and configure the `kai-queue-permissions` ConfigMap. + ## TODO: Log persistence Currently, logs are lost when RayJob pods are cleaned up (`ttlSecondsAfterFinished`). Two levels of log persistence are needed: From aefe47e6c0100a3ffd97d16ed87e93d4ba7a93fd Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Wed, 1 Apr 2026 01:06:43 -0700 Subject: [PATCH 06/84] infra: upgrade KAI to v0.14.0, k8s CLI, fixed Grafana dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Upgrade KAI scheduler v0.13.4 → v0.14.0 (adds Ray topology-aware scheduling, segment-size annotation support for PyTorchJob) - Update chart URL from NVIDIA/KAI-Scheduler to kai-scheduler/KAI-Scheduler - Fix Grafana dashboard metric names (add kai_ prefix to match actual Prometheus metric names). Verified: Grafana queries return live data. - New: extensions/k8s_cli/ — standalone Python CLI (pip installable): - nrl-k8s fairshare — show queue config (quota, limit, weight, priority) - nrl-k8s occupancy — show GPU allocation per node and per queue - nrl-k8s submit — submit gang-scheduled RayJob with optional --segment-size for topology-aware scheduling - 6 unit tests (mocked K8s API), all passing - Add TODO for NVL72 topology testing with links to relevant PRs/issues Tested: KAI v0.14.0 gang scheduling works, CLI commands verified against live cluster, Grafana dashboard loads and queries return data. Signed-off-by: Terry Kong --- extensions/k8s_cli/pyproject.toml | 19 ++ extensions/k8s_cli/src/nrl_k8s/__init__.py | 0 extensions/k8s_cli/src/nrl_k8s/cli.py | 149 ++++++++++ extensions/k8s_cli/src/nrl_k8s/k8s_client.py | 270 +++++++++++++++++++ extensions/k8s_cli/tests/__init__.py | 0 extensions/k8s_cli/tests/test_k8s_client.py | 209 ++++++++++++++ infra/examples/kai-grafana-dashboard.yaml | 21 +- infra/helm/helmfile.yaml | 2 +- infra/kind/SETUP.md | 13 +- 9 files changed, 667 insertions(+), 16 deletions(-) create mode 100644 extensions/k8s_cli/pyproject.toml create mode 100644 extensions/k8s_cli/src/nrl_k8s/__init__.py create mode 100644 extensions/k8s_cli/src/nrl_k8s/cli.py create mode 100644 extensions/k8s_cli/src/nrl_k8s/k8s_client.py create mode 100644 extensions/k8s_cli/tests/__init__.py create mode 100644 extensions/k8s_cli/tests/test_k8s_client.py diff --git a/extensions/k8s_cli/pyproject.toml b/extensions/k8s_cli/pyproject.toml new file mode 100644 index 00000000000..b9ef1e20941 --- /dev/null +++ b/extensions/k8s_cli/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "nrl-k8s" +version = "0.1.0" +description = "CLI for managing nemo-rl workloads on Kubernetes with KAI scheduler" +requires-python = ">=3.10" +dependencies = ["kubernetes>=28.0", "click>=8.0", "rich>=13.0"] + +[project.optional-dependencies] +dev = ["pytest>=8.0", "pytest-mock>=3.0"] + +[project.scripts] +nrl-k8s = "nrl_k8s.cli:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/nrl_k8s"] diff --git a/extensions/k8s_cli/src/nrl_k8s/__init__.py b/extensions/k8s_cli/src/nrl_k8s/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/k8s_cli/src/nrl_k8s/cli.py b/extensions/k8s_cli/src/nrl_k8s/cli.py new file mode 100644 index 00000000000..7518c1bb0aa --- /dev/null +++ b/extensions/k8s_cli/src/nrl_k8s/cli.py @@ -0,0 +1,149 @@ +# 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. +"""CLI for managing nemo-rl workloads on Kubernetes with KAI scheduler.""" + +import click +from rich.console import Console +from rich.table import Table + +from nrl_k8s.k8s_client import get_gpu_occupancy, get_queues, submit_gang_rayjob + +console = Console() + + +@click.group() +def main(): + """nrl-k8s: Manage nemo-rl workloads on Kubernetes with KAI scheduler.""" + + +@main.command() +def fairshare(): + """Show KAI scheduler queue fairshare configuration.""" + queues = get_queues() + table = Table(title="KAI Scheduler Queues (Fairshare)") + table.add_column("Queue", style="cyan") + table.add_column("Parent", style="dim") + table.add_column("Priority", justify="right") + table.add_column("GPU Quota", justify="right") + table.add_column("GPU Limit", justify="right") + table.add_column("Weight", justify="right") + table.add_column("Preempt Min", style="dim") + table.add_column("Reclaim Min", style="dim") + + for q in queues: + table.add_row( + q["name"], + q["parent"] or "-", + str(q["priority"]) if q["priority"] else "-", + str(q["gpu_quota"]), + str(q["gpu_limit"]), + str(q["gpu_weight"]), + q["preempt_min_runtime"] or "-", + q["reclaim_min_runtime"] or "-", + ) + + console.print(table) + + +@main.command() +def occupancy(): + """Show current GPU occupancy per node and per queue.""" + data = get_gpu_occupancy() + + # Node table. + node_table = Table(title="GPU Occupancy by Node") + node_table.add_column("Node", style="cyan") + node_table.add_column("Allocatable", justify="right") + node_table.add_column("Allocated", justify="right") + node_table.add_column("Free", justify="right", style="green") + + for n in data["nodes"]: + if n["allocatable"] > 0: + node_table.add_row( + n["name"], + str(n["allocatable"]), + str(n["allocated"]), + str(n["allocatable"] - n["allocated"]), + ) + + node_table.add_section() + node_table.add_row( + "TOTAL", + str(data["total_allocatable"]), + str(data["total_allocated"]), + str(data["total_allocatable"] - data["total_allocated"]), + style="bold", + ) + console.print(node_table) + + # Queue table. + if data["queues"]: + queue_table = Table(title="GPU Occupancy by Queue") + queue_table.add_column("Queue", style="cyan") + queue_table.add_column("Allocated GPUs", justify="right") + for q in data["queues"]: + queue_table.add_row(q["name"], str(q["allocated_gpus"])) + console.print(queue_table) + else: + console.print("[dim]No GPU workloads running.[/dim]") + + +@main.command() +@click.argument("name") +@click.option("--queue", required=True, help="KAI scheduler queue name") +@click.option("--image", required=True, help="Container image") +@click.option("--entrypoint", required=True, help="Entrypoint command") +@click.option("--num-gpus", required=True, type=int, help="Total GPUs requested") +@click.option("--gpus-per-worker", default=1, type=int, help="GPUs per worker pod") +@click.option("--namespace", default="default", help="Kubernetes namespace") +@click.option( + "--segment-size", + default=None, + type=int, + help="Topology segment size (nodes per rack). Creates PodGroup subgroups.", +) +def submit( + name, queue, image, entrypoint, num_gpus, gpus_per_worker, namespace, segment_size +): + """Submit a gang-scheduled RayJob.""" + console.print( + f"Submitting RayJob [cyan]{name}[/cyan] to queue [yellow]{queue}[/yellow]" + ) + console.print( + f" GPUs: {num_gpus} ({num_gpus // gpus_per_worker} workers × {gpus_per_worker} GPU/worker)" + ) + if segment_size: + import math + + num_segments = math.ceil(num_gpus // gpus_per_worker / segment_size) + console.print( + f" Segments: {num_segments} × {segment_size} workers (topology-constrained per rack)" + ) + + result_name = submit_gang_rayjob( + name=name, + queue=queue, + image=image, + entrypoint=entrypoint, + num_gpus=num_gpus, + gpus_per_worker=gpus_per_worker, + namespace=namespace, + segment_size=segment_size, + ) + console.print(f"[green]Created RayJob: {result_name}[/green]") + console.print(f"Watch: kubectl get rayjob {result_name} -w") + + +if __name__ == "__main__": + main() diff --git a/extensions/k8s_cli/src/nrl_k8s/k8s_client.py b/extensions/k8s_cli/src/nrl_k8s/k8s_client.py new file mode 100644 index 00000000000..77f38a66980 --- /dev/null +++ b/extensions/k8s_cli/src/nrl_k8s/k8s_client.py @@ -0,0 +1,270 @@ +# 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. +"""Thin wrapper around the Kubernetes API for KAI scheduler queries.""" + +from __future__ import annotations + +from kubernetes import client, config +from kubernetes.client.exceptions import ApiException + + +def load_k8s_config(): + """Load in-cluster or kubeconfig.""" + try: + config.load_incluster_config() + except config.ConfigException: + config.load_kube_config() + + +def get_queues(namespace: str | None = None) -> list[dict]: + """Fetch all KAI scheduler queues and their resource status.""" + load_k8s_config() + custom = client.CustomObjectsApi() + result = custom.list_cluster_custom_object( + group="scheduling.run.ai", version="v2", plural="queues" + ) + queues = [] + for q in result.get("items", []): + spec = q.get("spec", {}) + gpu = spec.get("resources", {}).get("gpu", {}) + queues.append( + { + "name": q["metadata"]["name"], + "parent": spec.get("parentQueue", ""), + "priority": spec.get("priority", ""), + "gpu_quota": gpu.get("quota", -1), + "gpu_limit": gpu.get("limit", -1), + "gpu_weight": gpu.get("overQuotaWeight", 1), + "preempt_min_runtime": spec.get("preemptMinRuntime", ""), + "reclaim_min_runtime": spec.get("reclaimMinRuntime", ""), + } + ) + return queues + + +def get_gpu_occupancy(namespace: str = "default") -> dict: + """Get current GPU allocation per node and per queue. + + Returns: + { + "nodes": [{"name": ..., "allocatable": N, "allocated": M}], + "queues": [{"name": ..., "allocated_gpus": N}], + "total_allocatable": N, + "total_allocated": M, + } + """ + load_k8s_config() + v1 = client.CoreV1Api() + + # Node-level GPU info. + nodes_info = [] + total_allocatable = 0 + total_allocated = 0 + nodes = v1.list_node() + for node in nodes.items: + alloc = int(node.status.allocatable.get("nvidia.com/gpu", "0")) + total_allocatable += alloc + nodes_info.append( + {"name": node.metadata.name, "allocatable": alloc, "allocated": 0} + ) + + # Count allocated GPUs per node from running pods. + pods = v1.list_pod_for_all_namespaces(field_selector="status.phase=Running") + for pod in pods.items: + node_name = pod.spec.node_name + for container in pod.spec.containers: + limits = container.resources.limits or {} + gpu_req = int(limits.get("nvidia.com/gpu", "0")) + if gpu_req > 0: + total_allocated += gpu_req + for n in nodes_info: + if n["name"] == node_name: + n["allocated"] += gpu_req + + # Queue-level allocation from PodGroups. + queue_alloc: dict[str, int] = {} + for pod in pods.items: + queue = ( + pod.metadata.labels.get("kai.scheduler/queue", "") + if pod.metadata.labels + else "" + ) + if not queue: + continue + for container in pod.spec.containers: + limits = container.resources.limits or {} + gpu_req = int(limits.get("nvidia.com/gpu", "0")) + queue_alloc[queue] = queue_alloc.get(queue, 0) + gpu_req + + return { + "nodes": nodes_info, + "queues": [ + {"name": k, "allocated_gpus": v} for k, v in sorted(queue_alloc.items()) + ], + "total_allocatable": total_allocatable, + "total_allocated": total_allocated, + } + + +def submit_gang_rayjob( + name: str, + queue: str, + image: str, + entrypoint: str, + num_gpus: int, + gpus_per_worker: int = 1, + namespace: str = "default", + segment_size: int | None = None, +) -> str: + """Submit a RayJob with gang scheduling via KAI. + + If segment_size is specified, creates a PodGroup with subgroups for + topology-aware segment scheduling (equivalent to Slurm --segment=N). + + Returns the created RayJob name. + """ + load_k8s_config() + custom = client.CustomObjectsApi() + + num_workers = num_gpus // gpus_per_worker + + rayjob = { + "apiVersion": "ray.io/v1", + "kind": "RayJob", + "metadata": { + "name": name, + "namespace": namespace, + "labels": {"kai.scheduler/queue": queue}, + }, + "spec": { + "entrypoint": entrypoint, + "submissionMode": "HTTPMode", + "shutdownAfterJobFinishes": True, + "ttlSecondsAfterFinished": 60, + "rayClusterSpec": { + "rayVersion": "2.52.0", + "headGroupSpec": { + "rayStartParams": {"object-store-memory": "200000000"}, + "template": { + "spec": { + "schedulerName": "kai-scheduler", + "containers": [ + { + "name": "ray-head", + "image": image, + "resources": { + "requests": {"cpu": "1", "memory": "4Gi"}, + "limits": {"cpu": "4", "memory": "16Gi"}, + }, + "ports": [ + {"containerPort": 6379, "name": "gcs-server"}, + {"containerPort": 8265, "name": "dashboard"}, + ], + } + ], + } + }, + }, + "workerGroupSpecs": [ + { + "groupName": "gpu-workers", + "replicas": num_workers, + "minReplicas": num_workers, + "maxReplicas": num_workers, + "rayStartParams": { + "num-gpus": str(gpus_per_worker), + "object-store-memory": "200000000", + }, + "template": { + "spec": { + "schedulerName": "kai-scheduler", + "containers": [ + { + "name": "ray-worker", + "image": image, + "resources": { + "requests": { + "cpu": "1", + "memory": "4Gi", + "nvidia.com/gpu": str(gpus_per_worker), + }, + "limits": { + "cpu": "8", + "memory": "32Gi", + "nvidia.com/gpu": str(gpus_per_worker), + }, + }, + } + ], + } + }, + } + ], + }, + }, + } + + result = custom.create_namespaced_custom_object( + group="ray.io", + version="v1", + namespace=namespace, + plural="rayjobs", + body=rayjob, + ) + + # If segment_size is specified, create a PodGroup with topology subgroups. + if segment_size and segment_size < num_workers: + import math + + num_segments = math.ceil(num_workers / segment_size) + cluster_name = result["status"].get("rayClusterName", name) + subgroups = [] + for i in range(num_segments): + subgroups.append( + { + "name": f"segment-{i}", + "minMember": min(segment_size, num_workers - i * segment_size), + "topologyConstraint": { + "topology": "cluster-topology", + "requiredTopologyLevel": "topology-rack", + }, + } + ) + + podgroup = { + "apiVersion": "scheduling.run.ai/v2alpha2", + "kind": "PodGroup", + "metadata": { + "name": f"pg-{name}", + "namespace": namespace, + }, + "spec": { + "minMember": num_workers, + "queue": queue, + "subGroups": subgroups, + }, + } + try: + custom.create_namespaced_custom_object( + group="scheduling.run.ai", + version="v2alpha2", + namespace=namespace, + plural="podgroups", + body=podgroup, + ) + except ApiException as e: + if e.status != 409: + raise + + return result["metadata"]["name"] diff --git a/extensions/k8s_cli/tests/__init__.py b/extensions/k8s_cli/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/k8s_cli/tests/test_k8s_client.py b/extensions/k8s_cli/tests/test_k8s_client.py new file mode 100644 index 00000000000..13d3f9f32a1 --- /dev/null +++ b/extensions/k8s_cli/tests/test_k8s_client.py @@ -0,0 +1,209 @@ +# 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. +"""Unit tests for nrl_k8s.k8s_client (mocked K8s API).""" + +from unittest.mock import MagicMock, patch + +import pytest + + +@pytest.fixture(autouse=True) +def mock_k8s_config(): + """Prevent real K8s config loading in all tests.""" + with patch("nrl_k8s.k8s_client.load_k8s_config"): + yield + + +class TestGetQueues: + def test_parses_queue_fields(self): + mock_custom = MagicMock() + mock_custom.list_cluster_custom_object.return_value = { + "items": [ + { + "metadata": {"name": "org"}, + "spec": { + "resources": { + "gpu": {"quota": -1, "limit": -1, "overQuotaWeight": 1} + } + }, + }, + { + "metadata": {"name": "priority-team"}, + "spec": { + "parentQueue": "org", + "priority": 200, + "preemptMinRuntime": "4h", + "reclaimMinRuntime": "15m", + "resources": { + "gpu": {"quota": 1, "limit": 2, "overQuotaWeight": 2} + }, + }, + }, + ] + } + + with patch( + "nrl_k8s.k8s_client.client.CustomObjectsApi", return_value=mock_custom + ): + from nrl_k8s.k8s_client import get_queues + + queues = get_queues() + + assert len(queues) == 2 + assert queues[0]["name"] == "org" + assert queues[0]["gpu_quota"] == -1 + assert queues[1]["name"] == "priority-team" + assert queues[1]["parent"] == "org" + assert queues[1]["priority"] == 200 + assert queues[1]["gpu_quota"] == 1 + assert queues[1]["gpu_limit"] == 2 + assert queues[1]["gpu_weight"] == 2 + assert queues[1]["preempt_min_runtime"] == "4h" + assert queues[1]["reclaim_min_runtime"] == "15m" + + def test_empty_cluster(self): + mock_custom = MagicMock() + mock_custom.list_cluster_custom_object.return_value = {"items": []} + + with patch( + "nrl_k8s.k8s_client.client.CustomObjectsApi", return_value=mock_custom + ): + from nrl_k8s.k8s_client import get_queues + + assert get_queues() == [] + + +class TestGetGpuOccupancy: + def _make_node(self, name, gpu_count): + node = MagicMock() + node.metadata.name = name + node.status.allocatable = {"nvidia.com/gpu": str(gpu_count)} + return node + + def _make_pod(self, name, node_name, gpu_count, queue=""): + pod = MagicMock() + pod.metadata.name = name + pod.spec.node_name = node_name + pod.metadata.labels = {"kai.scheduler/queue": queue} if queue else {} + container = MagicMock() + container.resources.limits = ( + {"nvidia.com/gpu": str(gpu_count)} if gpu_count else {} + ) + pod.spec.containers = [container] + return pod + + def test_counts_gpus(self): + mock_v1 = MagicMock() + nodes = MagicMock() + nodes.items = [self._make_node("worker-0", 2)] + mock_v1.list_node.return_value = nodes + + pods = MagicMock() + pods.items = [ + self._make_pod("pod-a", "worker-0", 1, queue="priority-team"), + self._make_pod("pod-b", "worker-0", 1, queue="community"), + ] + mock_v1.list_pod_for_all_namespaces.return_value = pods + + with patch("nrl_k8s.k8s_client.client.CoreV1Api", return_value=mock_v1): + from nrl_k8s.k8s_client import get_gpu_occupancy + + result = get_gpu_occupancy() + + assert result["total_allocatable"] == 2 + assert result["total_allocated"] == 2 + assert result["nodes"][0]["allocated"] == 2 + assert len(result["queues"]) == 2 + + def test_empty_cluster(self): + mock_v1 = MagicMock() + nodes = MagicMock() + nodes.items = [self._make_node("worker-0", 2)] + mock_v1.list_node.return_value = nodes + pods = MagicMock() + pods.items = [] + mock_v1.list_pod_for_all_namespaces.return_value = pods + + with patch("nrl_k8s.k8s_client.client.CoreV1Api", return_value=mock_v1): + from nrl_k8s.k8s_client import get_gpu_occupancy + + result = get_gpu_occupancy() + + assert result["total_allocated"] == 0 + assert result["queues"] == [] + + +class TestSubmitGangRayjob: + def test_creates_rayjob(self): + mock_custom = MagicMock() + mock_custom.create_namespaced_custom_object.return_value = { + "metadata": {"name": "test-job"}, + "status": {}, + } + + with patch( + "nrl_k8s.k8s_client.client.CustomObjectsApi", return_value=mock_custom + ): + from nrl_k8s.k8s_client import submit_gang_rayjob + + result = submit_gang_rayjob( + name="test-job", + queue="priority-team", + image="rayproject/ray:2.52.0", + entrypoint="echo hello", + num_gpus=2, + ) + + assert result == "test-job" + call_args = mock_custom.create_namespaced_custom_object.call_args + body = call_args.kwargs["body"] + assert body["metadata"]["labels"]["kai.scheduler/queue"] == "priority-team" + assert body["spec"]["submissionMode"] == "HTTPMode" + workers = body["spec"]["rayClusterSpec"]["workerGroupSpecs"][0] + assert workers["replicas"] == 2 + + def test_with_segment_size(self): + mock_custom = MagicMock() + mock_custom.create_namespaced_custom_object.return_value = { + "metadata": {"name": "seg-job"}, + "status": {"rayClusterName": "seg-job-abc"}, + } + + with patch( + "nrl_k8s.k8s_client.client.CustomObjectsApi", return_value=mock_custom + ): + from nrl_k8s.k8s_client import submit_gang_rayjob + + submit_gang_rayjob( + name="seg-job", + queue="priority-team", + image="rayproject/ray:2.52.0", + entrypoint="echo hello", + num_gpus=4, + segment_size=2, + ) + + # Should have 2 calls: RayJob + PodGroup + assert mock_custom.create_namespaced_custom_object.call_count == 2 + pg_call = mock_custom.create_namespaced_custom_object.call_args_list[1] + pg_body = pg_call.kwargs["body"] + assert pg_body["kind"] == "PodGroup" + assert len(pg_body["spec"]["subGroups"]) == 2 + assert pg_body["spec"]["subGroups"][0]["minMember"] == 2 + assert ( + pg_body["spec"]["subGroups"][0]["topologyConstraint"][ + "requiredTopologyLevel" + ] + == "topology-rack" + ) diff --git a/infra/examples/kai-grafana-dashboard.yaml b/infra/examples/kai-grafana-dashboard.yaml index d6bad06fb0c..3d3a1ae88c5 100644 --- a/infra/examples/kai-grafana-dashboard.yaml +++ b/infra/examples/kai-grafana-dashboard.yaml @@ -29,19 +29,14 @@ data: "gridPos": { "h": 8, "w": 24, "x": 0, "y": 0 }, "targets": [ { - "expr": "queue_allocated_gpus", + "expr": "kai_queue_allocated_gpus", "legendFormat": "{{queue_name}} allocated", "refId": "A" }, { - "expr": "queue_fair_share_gpu", - "legendFormat": "{{queue_name}} fair share", + "expr": "kai_queue_deserved_gpus", + "legendFormat": "{{queue_name}} deserved (fair share)", "refId": "B" - }, - { - "expr": "queue_deserved_gpus", - "legendFormat": "{{queue_name}} deserved (quota)", - "refId": "C" } ], "fieldConfig": { @@ -58,12 +53,12 @@ data: "gridPos": { "h": 10, "w": 24, "x": 0, "y": 8 }, "targets": [ { - "expr": "queue_allocated_gpus", + "expr": "kai_queue_allocated_gpus", "legendFormat": "{{queue_name}} allocated", "refId": "A" }, { - "expr": "queue_fair_share_gpu", + "expr": "kai_queue_deserved_gpus", "legendFormat": "{{queue_name}} fair share", "refId": "B" } @@ -82,12 +77,12 @@ data: "gridPos": { "h": 8, "w": 12, "x": 0, "y": 18 }, "targets": [ { - "expr": "rate(total_preemption_attempts[5m])", + "expr": "rate(kai_total_preemption_attempts[5m])", "legendFormat": "preemption attempts/s", "refId": "A" }, { - "expr": "rate(pod_group_evicted_pods_total[5m])", + "expr": "rate(kai_pod_group_evicted_pods_total[5m])", "legendFormat": "evictions/s ({{queue_name}})", "refId": "B" } @@ -103,7 +98,7 @@ data: "gridPos": { "h": 8, "w": 12, "x": 12, "y": 18 }, "targets": [ { - "expr": "e2e_scheduling_latency_milliseconds", + "expr": "kai_e2e_scheduling_latency_milliseconds", "legendFormat": "e2e latency (ms)", "refId": "A" } diff --git a/infra/helm/helmfile.yaml b/infra/helm/helmfile.yaml index f867272cb3a..0ac8858b6e0 100644 --- a/infra/helm/helmfile.yaml +++ b/infra/helm/helmfile.yaml @@ -54,7 +54,7 @@ releases: - name: kai-scheduler namespace: kai-scheduler createNamespace: true - chart: https://github.com/NVIDIA/KAI-Scheduler/releases/download/v0.13.4/kai-scheduler-v0.13.4.tgz + chart: https://github.com/kai-scheduler/KAI-Scheduler/releases/download/v0.14.0/kai-scheduler-v0.14.0.tgz wait: true values: - values/kai-scheduler.yaml diff --git a/infra/kind/SETUP.md b/infra/kind/SETUP.md index 3f7dff8e6d6..421f2d7b114 100644 --- a/infra/kind/SETUP.md +++ b/infra/kind/SETUP.md @@ -174,16 +174,25 @@ KAI distributes GPU resources using hierarchical fair-share with two phases: ```sh kubectl port-forward svc/kube-prometheus-stack-grafana -n monitoring 3000:80 +# Open http://localhost:3000 # Login: admin / prom-operator -# Dashboard: "KAI Scheduler Fairshare" +# Dashboard: search "KAI Scheduler Fairshare" or go to http://localhost:3000/d/kai-fairshare ``` -Key metrics: `queue_allocated_gpus`, `queue_fair_share_gpu`, `queue_deserved_gpus`, `total_preemption_attempts`. +Key metrics: `kai_queue_allocated_gpus`, `kai_queue_deserved_gpus`, `kai_e2e_scheduling_latency_milliseconds`. ### Kyverno queue enforcement RayCluster and RayJob resources must have a `kai.scheduler/queue` label or they're rejected by Kyverno. To enable user→queue access control, uncomment Policy 2 in `kyverno-kai-policies.yaml` and configure the `kai-queue-permissions` ConfigMap. +## TODO: NVL72 topology-aware scheduling + +KAI v0.14.0 added Ray topology-aware subgroup scheduling ([PR #1125](https://github.com/kai-scheduler/KAI-Scheduler/pull/1125)). Need to test on an actual NVL72 cluster: + +- **Confirm `--segment=N` equivalent works**: KAI's `subGroups` with per-subgroup `topologyConstraint.requiredTopologyLevel: "rack"` should be the equivalent of Slurm's `--segment=N`. Each subgroup of N nodes is constrained to one rack. Unclear if this works correctly for cross-rack scheduling (e.g., `--segment=16` with 32 total nodes = 2 racks). +- **Auto-segmentation not yet implemented**: The design doc at [`docs/developer/designs/segmented-subgroups/`](https://github.com/kai-scheduler/KAI-Scheduler/blob/main/docs/developer/designs/segmented-subgroups/README.md) proposes `kai.scheduler/segment-size` annotation for automatic subgroup creation, but it depends on "Replica-Type SubGrouping" which isn't shipped yet. See [Issue #1189](https://github.com/kai-scheduler/KAI-Scheduler/issues/1189) and [PR #1127](https://github.com/kai-scheduler/KAI-Scheduler/pull/1127) (minSubGroup field, still open). +- **Test with our k8s CLI**: The `nrl-k8s submit` command (see `extensions/k8s_cli/`) should support `--segment-size` that auto-generates the PodGroup subgroups until KAI ships native support. + ## TODO: Log persistence Currently, logs are lost when RayJob pods are cleaned up (`ttlSecondsAfterFinished`). Two levels of log persistence are needed: From 702d4e9106cab71ed3377dc161cd9f1721f90248 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Tue, 14 Apr 2026 20:11:26 -0700 Subject: [PATCH 07/84] infra: consolidate examples, inline peer-watcher, clean up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merge disagg_rl_raycluster.yaml + disagg_gym_raycluster.yaml into single disagg-rayclusters.yaml (always deployed together) - Inline peer-watcher Python script directly in sidecar container args (eliminates ConfigMap setup step, each deployment is self-contained) - Remove 7 redundant workload YAMLs (sft_rayjob, kai_scheduled_*, raycluster-blocker, standalone peer-watcher.py) - Update SETUP.md: simplified quick start, updated architecture tree, removed ConfigMap peer-watcher setup step 15 files → 8 files in examples/. Infrastructure configs unchanged. Tested: inlined peer-watcher works — deleting either cluster triggers teardown of both within 10s. Signed-off-by: Terry Kong --- extensions/k8s_cli/src/nrl_k8s/k8s_client.py | 8 + infra/examples/disagg-rayclusters.yaml | 261 ++++++++++++++++++ infra/examples/disagg_gym_raycluster.yaml | 73 ----- infra/examples/disagg_rl_raycluster.yaml | 120 -------- infra/examples/kai_scheduled_pods.yaml | 47 ---- infra/examples/kai_scheduled_rayclusters.yaml | 111 -------- infra/examples/kai_scheduled_sft.yaml | 127 --------- infra/examples/peer-watcher.py | 160 ----------- infra/examples/raycluster-blocker.yaml | 17 -- infra/examples/sft_rayjob.yaml | 83 ------ infra/kind/SETUP.md | 97 ++++--- 11 files changed, 316 insertions(+), 788 deletions(-) create mode 100644 infra/examples/disagg-rayclusters.yaml delete mode 100644 infra/examples/disagg_gym_raycluster.yaml delete mode 100644 infra/examples/disagg_rl_raycluster.yaml delete mode 100644 infra/examples/kai_scheduled_pods.yaml delete mode 100644 infra/examples/kai_scheduled_rayclusters.yaml delete mode 100644 infra/examples/kai_scheduled_sft.yaml delete mode 100644 infra/examples/peer-watcher.py delete mode 100644 infra/examples/raycluster-blocker.yaml delete mode 100644 infra/examples/sft_rayjob.yaml diff --git a/extensions/k8s_cli/src/nrl_k8s/k8s_client.py b/extensions/k8s_cli/src/nrl_k8s/k8s_client.py index 77f38a66980..6bac4861dda 100644 --- a/extensions/k8s_cli/src/nrl_k8s/k8s_client.py +++ b/extensions/k8s_cli/src/nrl_k8s/k8s_client.py @@ -171,6 +171,14 @@ def submit_gang_rayjob( {"containerPort": 6379, "name": "gcs-server"}, {"containerPort": 8265, "name": "dashboard"}, ], + "readinessProbe": { + "exec": { + "command": ["ray", "health-check"], + }, + "initialDelaySeconds": 10, + "periodSeconds": 5, + "timeoutSeconds": 5, + }, } ], } diff --git a/infra/examples/disagg-rayclusters.yaml b/infra/examples/disagg-rayclusters.yaml new file mode 100644 index 00000000000..057f4111cb0 --- /dev/null +++ b/infra/examples/disagg-rayclusters.yaml @@ -0,0 +1,261 @@ +# Disaggregated RL + Gym deployment: two gang-scheduled RayClusters. +# +# RL cluster: Ray head + GPU workers for training (vLLM + Megatron). +# Gym cluster: CPU-only, runs NeMo Gym servers as a standalone HTTP service. +# +# Service discovery: K8s ConfigMap endpoint registry (see endpoint-registry-rbac.yaml). +# RL registers vLLM URLs, Gym registers its head server address. Both poll until +# the peer's address appears. +# +# Failure cascading: peer-watcher sidecar on each head pod monitors the other cluster +# via K8s API. If either is deleted/failed, both are torn down to release resources. +# +# Prerequisites: +# kubectl apply -f endpoint-registry-rbac.yaml +# +# Usage: +# kubectl apply -f disagg-rayclusters.yaml +# # On RL head: run GRPO with +env.disagg_job_id= +# # On Gym head: run standalone_server.py with --job-id= + +# ======================== +# RL RayCluster (GPU) +# ======================== +apiVersion: ray.io/v1 +kind: RayCluster +metadata: + name: raycluster-rl + labels: + kai.scheduler/queue: priority-team # KAI fairshare queue +spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: + object-store-memory: "200000000" + template: + spec: + serviceAccountName: nemo-rl-endpoint-registry # RBAC for ConfigMap + RayCluster access + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + containers: + # --- Ray head --- + - name: ray-head + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + env: + - name: RAY_CLUSTER_NAME + value: raycluster-rl # used by endpoint registry for ownerReference + resources: + limits: { cpu: "4", memory: "16Gi" } + requests: { cpu: "1", memory: "4Gi" } + ports: + - { containerPort: 6379, name: gcs-server } + - { containerPort: 8265, name: dashboard } + - { containerPort: 10001, name: client } + volumeMounts: + - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + # --- Peer-watcher sidecar: tears down both clusters if Gym fails --- + # Monitors raycluster-gym via K8s API. If peer is deleted, failed, or + # signals an error via ConfigMap, deletes both RayClusters. + - name: peer-watcher + image: python:3.12-slim + command: ["python3", "-u", "-c"] + args: + - | + import json, os, ssl, sys, time, urllib.request + from pathlib import Path + + SELF = os.environ["SELF_CLUSTER_NAME"] + PEER = os.environ["PEER_CLUSTER_NAME"] + JOB_ID = os.environ.get("JOB_ID", "") + INTERVAL = int(os.environ.get("POLL_INTERVAL", "10")) + MAX_FAIL = int(os.environ.get("MAX_PEER_FAILURES", "3")) + + SA = Path("/var/run/secrets/kubernetes.io/serviceaccount") + NS = (SA / "namespace").read_text().strip() if (SA / "namespace").exists() else "default" + TOKEN = (SA / "token").read_text().strip() if (SA / "token").exists() else "" + API = "https://kubernetes.default.svc" + CTX = ssl.create_default_context(cafile=str(SA / "ca.crt")) if (SA / "ca.crt").exists() else ssl._create_unverified_context() + + def kube(path, method="GET"): + req = urllib.request.Request(f"{API}{path}", method=method) + req.add_header("Authorization", f"Bearer {TOKEN}") + try: + with urllib.request.urlopen(req, context=CTX, timeout=10) as r: + return json.loads(r.read()) + except urllib.error.HTTPError as e: + return {"code": e.code} + except Exception: + return {"code": 0} + + def teardown(reason): + print(f"[peer-watcher] TEARDOWN: {reason}", flush=True) + for n in (SELF, PEER): + kube(f"/apis/ray.io/v1/namespaces/{NS}/rayclusters/{n}", "DELETE") + if JOB_ID: + kube(f"/api/v1/namespaces/{NS}/configmaps/nemo-rl-endpoints-{JOB_ID}", "DELETE") + sys.exit(1) + + print(f"[peer-watcher] self={SELF} peer={PEER} ns={NS} poll={INTERVAL}s max_fail={MAX_FAIL}", flush=True) + fails = 0 + while True: + time.sleep(INTERVAL) + r = kube(f"/apis/ray.io/v1/namespaces/{NS}/rayclusters/{PEER}") + code = r.get("code", 0) + if code == 404: + teardown(f"Peer {PEER} deleted") + state = r.get("status", {}).get("state", "") + if state in ("failed", "suspended") or (code != 0 and state == ""): + fails += 1 + print(f"[peer-watcher] Peer {PEER} {state or 'unreachable'} ({fails}/{MAX_FAIL})", flush=True) + if fails >= MAX_FAIL: + teardown(f"Peer {PEER} failed {MAX_FAIL}x") + continue + if JOB_ID: + cm = kube(f"/api/v1/namespaces/{NS}/configmaps/nemo-rl-endpoints-{JOB_ID}") + if cm.get("data", {}).get("error", ""): + teardown(f"Error via ConfigMap: {cm['data']['error']}") + if fails > 0: + print(f"[peer-watcher] Peer {PEER} recovered", flush=True) + fails = 0 + env: + - { name: SELF_CLUSTER_NAME, value: raycluster-rl } + - { name: PEER_CLUSTER_NAME, value: raycluster-gym } + - { name: POLL_INTERVAL, value: "10" } + - { name: MAX_PEER_FAILURES, value: "3" } + # Uncomment to also clean up the ConfigMap on teardown: + # - { name: JOB_ID, value: "my-job-id" } + resources: + requests: { cpu: "50m", memory: "64Mi" } + limits: { cpu: "100m", memory: "128Mi" } + volumes: + - name: nemo-rl-src + hostPath: { path: /workspace/nemo-rl, type: DirectoryOrCreate } + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: + num-gpus: "2" + object-store-memory: "200000000" + template: + spec: + serviceAccountName: nemo-rl-endpoint-registry + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + containers: + - name: ray-worker + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + resources: + limits: { cpu: "8", memory: "32Gi", nvidia.com/gpu: "2" } + requests: { cpu: "2", memory: "8Gi", nvidia.com/gpu: "2" } + volumeMounts: + - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + volumes: + - name: nemo-rl-src + hostPath: { path: /workspace/nemo-rl, type: DirectoryOrCreate } + +--- +# ======================== +# Gym RayCluster (CPU only) +# ======================== +apiVersion: ray.io/v1 +kind: RayCluster +metadata: + name: raycluster-gym + labels: + kai.scheduler/queue: priority-team +spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: + object-store-memory: "200000000" + template: + spec: + serviceAccountName: nemo-rl-endpoint-registry + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + containers: + # --- Gym Ray head (runs standalone_server.py) --- + - name: ray-head + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + resources: + limits: { cpu: "4", memory: "16Gi" } + requests: { cpu: "1", memory: "4Gi" } + ports: + - { containerPort: 6379, name: gcs-server } + - { containerPort: 8265, name: dashboard } + - { containerPort: 9090, name: head-server } # Gym head server port + # --- Peer-watcher sidecar: tears down both clusters if RL fails --- + - name: peer-watcher + image: python:3.12-slim + command: ["python3", "-u", "-c"] + args: + - | + import json, os, ssl, sys, time, urllib.request + from pathlib import Path + + SELF = os.environ["SELF_CLUSTER_NAME"] + PEER = os.environ["PEER_CLUSTER_NAME"] + JOB_ID = os.environ.get("JOB_ID", "") + INTERVAL = int(os.environ.get("POLL_INTERVAL", "10")) + MAX_FAIL = int(os.environ.get("MAX_PEER_FAILURES", "3")) + + SA = Path("/var/run/secrets/kubernetes.io/serviceaccount") + NS = (SA / "namespace").read_text().strip() if (SA / "namespace").exists() else "default" + TOKEN = (SA / "token").read_text().strip() if (SA / "token").exists() else "" + API = "https://kubernetes.default.svc" + CTX = ssl.create_default_context(cafile=str(SA / "ca.crt")) if (SA / "ca.crt").exists() else ssl._create_unverified_context() + + def kube(path, method="GET"): + req = urllib.request.Request(f"{API}{path}", method=method) + req.add_header("Authorization", f"Bearer {TOKEN}") + try: + with urllib.request.urlopen(req, context=CTX, timeout=10) as r: + return json.loads(r.read()) + except urllib.error.HTTPError as e: + return {"code": e.code} + except Exception: + return {"code": 0} + + def teardown(reason): + print(f"[peer-watcher] TEARDOWN: {reason}", flush=True) + for n in (SELF, PEER): + kube(f"/apis/ray.io/v1/namespaces/{NS}/rayclusters/{n}", "DELETE") + if JOB_ID: + kube(f"/api/v1/namespaces/{NS}/configmaps/nemo-rl-endpoints-{JOB_ID}", "DELETE") + sys.exit(1) + + print(f"[peer-watcher] self={SELF} peer={PEER} ns={NS} poll={INTERVAL}s max_fail={MAX_FAIL}", flush=True) + fails = 0 + while True: + time.sleep(INTERVAL) + r = kube(f"/apis/ray.io/v1/namespaces/{NS}/rayclusters/{PEER}") + code = r.get("code", 0) + if code == 404: + teardown(f"Peer {PEER} deleted") + state = r.get("status", {}).get("state", "") + if state in ("failed", "suspended") or (code != 0 and state == ""): + fails += 1 + print(f"[peer-watcher] Peer {PEER} {state or 'unreachable'} ({fails}/{MAX_FAIL})", flush=True) + if fails >= MAX_FAIL: + teardown(f"Peer {PEER} failed {MAX_FAIL}x") + continue + if JOB_ID: + cm = kube(f"/api/v1/namespaces/{NS}/configmaps/nemo-rl-endpoints-{JOB_ID}") + if cm.get("data", {}).get("error", ""): + teardown(f"Error via ConfigMap: {cm['data']['error']}") + if fails > 0: + print(f"[peer-watcher] Peer {PEER} recovered", flush=True) + fails = 0 + env: + - { name: SELF_CLUSTER_NAME, value: raycluster-gym } + - { name: PEER_CLUSTER_NAME, value: raycluster-rl } + - { name: POLL_INTERVAL, value: "10" } + - { name: MAX_PEER_FAILURES, value: "3" } + resources: + requests: { cpu: "50m", memory: "64Mi" } + limits: { cpu: "100m", memory: "128Mi" } diff --git a/infra/examples/disagg_gym_raycluster.yaml b/infra/examples/disagg_gym_raycluster.yaml deleted file mode 100644 index eaa951ff482..00000000000 --- a/infra/examples/disagg_gym_raycluster.yaml +++ /dev/null @@ -1,73 +0,0 @@ -# Disaggregated Gym RayCluster with peer-watcher sidecar. -# Runs NeMo Gym servers as a standalone HTTP service. -# Uses K8s ConfigMap endpoint registry for service discovery with the RL cluster. -# The peer-watcher sidecar monitors raycluster-rl and tears down both clusters -# if the RL cluster fails or the application signals an error. -# -# Prerequisites: -# kubectl apply -f endpoint-registry-rbac.yaml -apiVersion: ray.io/v1 -kind: RayCluster -metadata: - name: raycluster-gym - labels: - kai.scheduler/queue: priority-team -spec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: - object-store-memory: "200000000" - template: - spec: - serviceAccountName: nemo-rl-endpoint-registry - schedulerName: kai-scheduler - imagePullSecrets: - - name: nvcr-secret - containers: - - name: ray-head - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 - resources: - limits: - cpu: "4" - memory: "16Gi" - requests: - cpu: "1" - memory: "4Gi" - ports: - - containerPort: 6379 - name: gcs-server - - containerPort: 8265 - name: dashboard - - containerPort: 9090 - name: head-server - volumeMounts: - - name: peer-watcher-script - mountPath: /opt/peer-watcher - # Sidecar: monitors raycluster-rl, tears down both on failure. - - name: peer-watcher - image: python:3.12-slim - command: ["python3", "/opt/peer-watcher/peer-watcher.py"] - env: - - name: SELF_CLUSTER_NAME - value: raycluster-gym - - name: PEER_CLUSTER_NAME - value: raycluster-rl - - name: POLL_INTERVAL - value: "10" - - name: MAX_PEER_FAILURES - value: "3" - resources: - requests: - cpu: "50m" - memory: "64Mi" - limits: - cpu: "100m" - memory: "128Mi" - volumeMounts: - - name: peer-watcher-script - mountPath: /opt/peer-watcher - volumes: - - name: peer-watcher-script - configMap: - name: peer-watcher-script - defaultMode: 0755 diff --git a/infra/examples/disagg_rl_raycluster.yaml b/infra/examples/disagg_rl_raycluster.yaml deleted file mode 100644 index b4b30cf8622..00000000000 --- a/infra/examples/disagg_rl_raycluster.yaml +++ /dev/null @@ -1,120 +0,0 @@ -# Disaggregated RL RayCluster with peer-watcher sidecar. -# Runs nemo-rl GRPO training with vLLM workers. -# Uses K8s ConfigMap endpoint registry for service discovery with the Gym cluster. -# The peer-watcher sidecar monitors raycluster-gym and tears down both clusters -# if the Gym cluster fails or the application signals an error. -# -# Prerequisites: -# kubectl apply -f endpoint-registry-rbac.yaml -apiVersion: ray.io/v1 -kind: RayCluster -metadata: - name: raycluster-rl - labels: - kai.scheduler/queue: priority-team -spec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: - object-store-memory: "200000000" - template: - spec: - serviceAccountName: nemo-rl-endpoint-registry - schedulerName: kai-scheduler - imagePullSecrets: - - name: nvcr-secret - containers: - - name: ray-head - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 - env: - - name: RAY_CLUSTER_NAME - value: raycluster-rl - resources: - limits: - cpu: "4" - memory: "16Gi" - requests: - cpu: "1" - memory: "4Gi" - ports: - - containerPort: 6379 - name: gcs-server - - containerPort: 8265 - name: dashboard - - containerPort: 10001 - name: client - volumeMounts: - - name: nemo-rl-src - mountPath: /workspace/nemo-rl - - name: peer-watcher-script - mountPath: /opt/peer-watcher - # Sidecar: monitors raycluster-gym, tears down both on failure. - - name: peer-watcher - image: python:3.12-slim - command: ["python3", "/opt/peer-watcher/peer-watcher.py"] - env: - - name: SELF_CLUSTER_NAME - value: raycluster-rl - - name: PEER_CLUSTER_NAME - value: raycluster-gym - - name: POLL_INTERVAL - value: "10" - - name: MAX_PEER_FAILURES - value: "3" - # JOB_ID should be set to match +env.disagg_job_id in the GRPO command. - # Uncomment and set when using the endpoint registry. - # - name: JOB_ID - # value: "my-job-id" - resources: - requests: - cpu: "50m" - memory: "64Mi" - limits: - cpu: "100m" - memory: "128Mi" - volumeMounts: - - name: peer-watcher-script - mountPath: /opt/peer-watcher - volumes: - - name: nemo-rl-src - hostPath: - path: /workspace/nemo-rl - type: DirectoryOrCreate - - name: peer-watcher-script - configMap: - name: peer-watcher-script - defaultMode: 0755 - workerGroupSpecs: - - groupName: gpu-workers - replicas: 1 - minReplicas: 1 - maxReplicas: 1 - rayStartParams: - num-gpus: "2" - object-store-memory: "200000000" - template: - spec: - serviceAccountName: nemo-rl-endpoint-registry - schedulerName: kai-scheduler - imagePullSecrets: - - name: nvcr-secret - containers: - - name: ray-worker - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 - resources: - limits: - cpu: "8" - memory: "32Gi" - nvidia.com/gpu: "2" - requests: - cpu: "2" - memory: "8Gi" - nvidia.com/gpu: "2" - volumeMounts: - - name: nemo-rl-src - mountPath: /workspace/nemo-rl - volumes: - - name: nemo-rl-src - hostPath: - path: /workspace/nemo-rl - type: DirectoryOrCreate diff --git a/infra/examples/kai_scheduled_pods.yaml b/infra/examples/kai_scheduled_pods.yaml deleted file mode 100644 index aeceda565cc..00000000000 --- a/infra/examples/kai_scheduled_pods.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Gang-schedule two GPU pods via KAI scheduler. -# Both pods are scheduled simultaneously or not at all. -apiVersion: scheduling.run.ai/v2alpha2 -kind: PodGroup -metadata: - name: gpu-test-group -spec: - minMember: 2 - queue: priority-team ---- -apiVersion: v1 -kind: Pod -metadata: - name: gpu-test-0 - labels: - kai.scheduler/queue: priority-team - annotations: - pod-group-name: gpu-test-group -spec: - schedulerName: kai-scheduler - restartPolicy: Never - containers: - - name: gpu-test - image: nvidia/cuda:12.8.1-base-ubuntu24.04 - command: ["nvidia-smi"] - resources: - limits: - nvidia.com/gpu: "1" ---- -apiVersion: v1 -kind: Pod -metadata: - name: gpu-test-1 - labels: - kai.scheduler/queue: priority-team - annotations: - pod-group-name: gpu-test-group -spec: - schedulerName: kai-scheduler - restartPolicy: Never - containers: - - name: gpu-test - image: nvidia/cuda:12.8.1-base-ubuntu24.04 - command: ["nvidia-smi"] - resources: - limits: - nvidia.com/gpu: "1" diff --git a/infra/examples/kai_scheduled_rayclusters.yaml b/infra/examples/kai_scheduled_rayclusters.yaml deleted file mode 100644 index 2e91a189566..00000000000 --- a/infra/examples/kai_scheduled_rayclusters.yaml +++ /dev/null @@ -1,111 +0,0 @@ -# Gang-schedule two RayClusters via KAI scheduler. -# Each cluster gets 1 GPU worker. Both clusters should come up simultaneously. -apiVersion: ray.io/v1 -kind: RayCluster -metadata: - name: raycluster-a - labels: - kai.scheduler/queue: priority-team -spec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: - object-store-memory: "100000000" # 100MB, minimum for kind - template: - spec: - schedulerName: kai-scheduler - containers: - - name: ray-head - image: rayproject/ray:2.52.0 - resources: - limits: - cpu: "1" - memory: "2Gi" - requests: - cpu: "500m" - memory: "512Mi" - ports: - - containerPort: 6379 - name: gcs-server - - containerPort: 8265 - name: dashboard - - containerPort: 10001 - name: client - workerGroupSpecs: - - groupName: gpu-workers - replicas: 1 - minReplicas: 1 - maxReplicas: 1 - rayStartParams: - num-gpus: "1" - object-store-memory: "100000000" - template: - spec: - schedulerName: kai-scheduler - containers: - - name: ray-worker - image: rayproject/ray:2.52.0-gpu - resources: - limits: - cpu: "1" - memory: "2Gi" - nvidia.com/gpu: "1" - requests: - cpu: "500m" - memory: "512Mi" - nvidia.com/gpu: "1" ---- -apiVersion: ray.io/v1 -kind: RayCluster -metadata: - name: raycluster-b - labels: - kai.scheduler/queue: priority-team -spec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: - object-store-memory: "100000000" - template: - spec: - schedulerName: kai-scheduler - containers: - - name: ray-head - image: rayproject/ray:2.52.0 - resources: - limits: - cpu: "1" - memory: "2Gi" - requests: - cpu: "500m" - memory: "512Mi" - ports: - - containerPort: 6379 - name: gcs-server - - containerPort: 8265 - name: dashboard - - containerPort: 10001 - name: client - workerGroupSpecs: - - groupName: gpu-workers - replicas: 1 - minReplicas: 1 - maxReplicas: 1 - rayStartParams: - num-gpus: "1" - object-store-memory: "100000000" - template: - spec: - schedulerName: kai-scheduler - containers: - - name: ray-worker - image: rayproject/ray:2.52.0-gpu - resources: - limits: - cpu: "1" - memory: "2Gi" - nvidia.com/gpu: "1" - requests: - cpu: "500m" - memory: "512Mi" - nvidia.com/gpu: "1" diff --git a/infra/examples/kai_scheduled_sft.yaml b/infra/examples/kai_scheduled_sft.yaml deleted file mode 100644 index ea678b63df8..00000000000 --- a/infra/examples/kai_scheduled_sft.yaml +++ /dev/null @@ -1,127 +0,0 @@ -# Gang-schedule two 1-GPU SFT RayJobs via KAI scheduler. -# Each runs SFT with cluster.gpus_per_node=1, then tears down. -apiVersion: ray.io/v1 -kind: RayJob -metadata: - name: sft-job-a - labels: - kai.scheduler/queue: team-a -spec: - entrypoint: "cd /opt/nemo-rl && bash tests/functional/sft.sh cluster.gpus_per_node=1" - # HTTPMode required with KAI scheduler (K8sJobMode breaks gang scheduling). - submissionMode: HTTPMode - shutdownAfterJobFinishes: true - ttlSecondsAfterFinished: 60 - rayClusterSpec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: - object-store-memory: "200000000" - template: - spec: - schedulerName: kai-scheduler - imagePullSecrets: - - name: nvcr-secret - containers: - - name: ray-head - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 - resources: - limits: - cpu: "4" - memory: "16Gi" - requests: - cpu: "1" - memory: "4Gi" - ports: - - containerPort: 6379 - name: gcs-server - - containerPort: 8265 - name: dashboard - workerGroupSpecs: - - groupName: gpu-workers - replicas: 1 - minReplicas: 1 - maxReplicas: 1 - rayStartParams: - num-gpus: "1" - object-store-memory: "200000000" - template: - spec: - schedulerName: kai-scheduler - imagePullSecrets: - - name: nvcr-secret - containers: - - name: ray-worker - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 - resources: - limits: - cpu: "4" - memory: "16Gi" - nvidia.com/gpu: "1" - requests: - cpu: "1" - memory: "4Gi" - nvidia.com/gpu: "1" ---- -apiVersion: ray.io/v1 -kind: RayJob -metadata: - name: sft-job-b - labels: - kai.scheduler/queue: team-a -spec: - entrypoint: "cd /opt/nemo-rl && bash tests/functional/sft.sh cluster.gpus_per_node=1" - # HTTPMode required with KAI scheduler (K8sJobMode breaks gang scheduling). - submissionMode: HTTPMode - shutdownAfterJobFinishes: true - ttlSecondsAfterFinished: 60 - rayClusterSpec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: - object-store-memory: "200000000" - template: - spec: - schedulerName: kai-scheduler - imagePullSecrets: - - name: nvcr-secret - containers: - - name: ray-head - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 - resources: - limits: - cpu: "4" - memory: "16Gi" - requests: - cpu: "1" - memory: "4Gi" - ports: - - containerPort: 6379 - name: gcs-server - - containerPort: 8265 - name: dashboard - workerGroupSpecs: - - groupName: gpu-workers - replicas: 1 - minReplicas: 1 - maxReplicas: 1 - rayStartParams: - num-gpus: "1" - object-store-memory: "200000000" - template: - spec: - schedulerName: kai-scheduler - imagePullSecrets: - - name: nvcr-secret - containers: - - name: ray-worker - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 - resources: - limits: - cpu: "4" - memory: "16Gi" - nvidia.com/gpu: "1" - requests: - cpu: "1" - memory: "4Gi" - nvidia.com/gpu: "1" diff --git a/infra/examples/peer-watcher.py b/infra/examples/peer-watcher.py deleted file mode 100644 index 97a2e7371ec..00000000000 --- a/infra/examples/peer-watcher.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python3 -# 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. -"""Sidecar that monitors a peer RayCluster and tears down both clusters on failure. - -Used for disaggregated RL-Gym setups where both clusters should fail fast together. -Runs as a sidecar container in the head pod of each RayCluster. - -Monitors: - 1. Peer RayCluster status (deleted / failed / suspended) - 2. ConfigMap "error" key (set by the application via K8sEndpointRegistry.signal_error()) - -Environment variables: - SELF_CLUSTER_NAME - Name of this RayCluster (to self-delete) - PEER_CLUSTER_NAME - Name of the peer RayCluster to watch - JOB_ID - Job ID for the ConfigMap endpoint registry (optional) - POLL_INTERVAL - Seconds between status checks (default: 10) - MAX_PEER_FAILURES - Consecutive failures before teardown (default: 3) -""" - -import json -import os -import ssl -import sys -import time -import urllib.request -from pathlib import Path - -SELF_CLUSTER_NAME = os.environ["SELF_CLUSTER_NAME"] -PEER_CLUSTER_NAME = os.environ["PEER_CLUSTER_NAME"] -JOB_ID = os.environ.get("JOB_ID", "") -POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL", "10")) -MAX_PEER_FAILURES = int(os.environ.get("MAX_PEER_FAILURES", "3")) - -SA_PATH = Path("/var/run/secrets/kubernetes.io/serviceaccount") -NAMESPACE = ( - (SA_PATH / "namespace").read_text().strip() - if (SA_PATH / "namespace").exists() - else "default" -) -TOKEN = (SA_PATH / "token").read_text().strip() if (SA_PATH / "token").exists() else "" -APISERVER = "https://kubernetes.default.svc" - -# Trust the in-cluster CA. -SSL_CTX = ( - ssl.create_default_context(cafile=str(SA_PATH / "ca.crt")) - if (SA_PATH / "ca.crt").exists() - else ssl._create_unverified_context() -) - - -def kube_request(path: str, method: str = "GET") -> dict: - req = urllib.request.Request(f"{APISERVER}{path}", method=method) - req.add_header("Authorization", f"Bearer {TOKEN}") - try: - with urllib.request.urlopen(req, context=SSL_CTX, timeout=10) as resp: - return json.loads(resp.read()) - except urllib.error.HTTPError as e: - return {"code": e.code, "message": e.reason} - except Exception as e: - return {"code": 0, "message": str(e)} - - -def teardown(reason: str): - print(f"[peer-watcher] TEARING DOWN: {reason}", flush=True) - path_prefix = f"/apis/ray.io/v1/namespaces/{NAMESPACE}/rayclusters" - for name in (SELF_CLUSTER_NAME, PEER_CLUSTER_NAME): - print(f"[peer-watcher] Deleting RayCluster: {name}", flush=True) - kube_request(f"{path_prefix}/{name}", method="DELETE") - if JOB_ID: - print( - f"[peer-watcher] Deleting ConfigMap: nemo-rl-endpoints-{JOB_ID}", flush=True - ) - kube_request( - f"/api/v1/namespaces/{NAMESPACE}/configmaps/nemo-rl-endpoints-{JOB_ID}", - method="DELETE", - ) - sys.exit(1) - - -def main(): - print( - f"[peer-watcher] Watching peer={PEER_CLUSTER_NAME}, self={SELF_CLUSTER_NAME}", - flush=True, - ) - print( - f"[peer-watcher] namespace={NAMESPACE}, poll={POLL_INTERVAL}s, max_failures={MAX_PEER_FAILURES}", - flush=True, - ) - - consecutive_failures = 0 - - while True: - time.sleep(POLL_INTERVAL) - - # Check peer RayCluster. - resp = kube_request( - f"/apis/ray.io/v1/namespaces/{NAMESPACE}/rayclusters/{PEER_CLUSTER_NAME}" - ) - code = resp.get("code", 0) - if code == 404: - teardown(f"Peer {PEER_CLUSTER_NAME} not found (deleted)") - - status = resp.get("status", {}).get("state", "") - if status in ("failed", "suspended"): - consecutive_failures += 1 - print( - f"[peer-watcher] Peer {PEER_CLUSTER_NAME} is {status} ({consecutive_failures}/{MAX_PEER_FAILURES})", - flush=True, - ) - if consecutive_failures >= MAX_PEER_FAILURES: - teardown( - f"Peer {PEER_CLUSTER_NAME} failed {MAX_PEER_FAILURES} consecutive checks" - ) - continue - - # Check ConfigMap error signal. - if JOB_ID: - cm = kube_request( - f"/api/v1/namespaces/{NAMESPACE}/configmaps/nemo-rl-endpoints-{JOB_ID}" - ) - error = cm.get("data", {}).get("error", "") - if error: - teardown(f"Error signaled via ConfigMap: {error}") - - # Unknown state (e.g., K8s API error, transient network issue) — treat as failure. - if code != 0 and status == "": - consecutive_failures += 1 - print( - f"[peer-watcher] Could not determine peer status (code={code}), treating as failure ({consecutive_failures}/{MAX_PEER_FAILURES})", - flush=True, - ) - if consecutive_failures >= MAX_PEER_FAILURES: - teardown( - f"Peer {PEER_CLUSTER_NAME} unreachable for {MAX_PEER_FAILURES} consecutive checks" - ) - continue - - # Peer healthy. - if consecutive_failures > 0: - print( - f"[peer-watcher] Peer {PEER_CLUSTER_NAME} recovered (status={status})", - flush=True, - ) - consecutive_failures = 0 - - -if __name__ == "__main__": - main() diff --git a/infra/examples/raycluster-blocker.yaml b/infra/examples/raycluster-blocker.yaml deleted file mode 100644 index c83a4013b64..00000000000 --- a/infra/examples/raycluster-blocker.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# A simple pod that occupies 1 GPU, used to test KAI gang scheduling. -# Uses the default scheduler (not KAI) so it won't be preempted. -# Deploy this first, then try to deploy kai_scheduled_rayclusters.yaml. -# With only 1 GPU free, KAI should hold BOTH rayclusters pending (all-or-nothing). -apiVersion: v1 -kind: Pod -metadata: - name: gpu-blocker -spec: - restartPolicy: Never - containers: - - name: blocker - image: nvidia/cuda:12.8.1-base-ubuntu24.04 - command: ["sleep", "infinity"] - resources: - limits: - nvidia.com/gpu: "1" diff --git a/infra/examples/sft_rayjob.yaml b/infra/examples/sft_rayjob.yaml deleted file mode 100644 index 030f422fcab..00000000000 --- a/infra/examples/sft_rayjob.yaml +++ /dev/null @@ -1,83 +0,0 @@ -# 2-GPU RayJob for running nemo-rl SFT training. -# Creates a cluster, runs SFT, then tears down automatically. -apiVersion: ray.io/v1 -kind: RayJob -metadata: - name: sft-job - labels: - kai.scheduler/queue: priority-team -spec: - entrypoint: "cd /opt/nemo-rl && bash tests/functional/sft.sh" - # HTTPMode is required with KAI scheduler — K8sJobMode creates a separate - # submitter pod after the cluster is ready, which breaks gang scheduling. - submissionMode: HTTPMode - shutdownAfterJobFinishes: true - ttlSecondsAfterFinished: 60 - rayClusterSpec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: - object-store-memory: "200000000" - template: - spec: - schedulerName: kai-scheduler - imagePullSecrets: - - name: nvcr-secret - containers: - - name: ray-head - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 - resources: - limits: - cpu: "4" - memory: "16Gi" - requests: - cpu: "1" - memory: "4Gi" - ports: - - containerPort: 6379 - name: gcs-server - - containerPort: 8265 - name: dashboard - - containerPort: 10001 - name: client - volumeMounts: - - name: nemo-rl-src - mountPath: /workspace/nemo-rl - volumes: - - name: nemo-rl-src - hostPath: - path: /workspace/nemo-rl - type: DirectoryOrCreate - workerGroupSpecs: - - groupName: gpu-workers - replicas: 1 - minReplicas: 1 - maxReplicas: 1 - rayStartParams: - num-gpus: "2" - object-store-memory: "200000000" - template: - spec: - schedulerName: kai-scheduler - imagePullSecrets: - - name: nvcr-secret - containers: - - name: ray-worker - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 - resources: - limits: - cpu: "8" - memory: "32Gi" - nvidia.com/gpu: "2" - requests: - cpu: "2" - memory: "8Gi" - nvidia.com/gpu: "2" - volumeMounts: - - name: nemo-rl-src - mountPath: /workspace/nemo-rl - volumes: - - name: nemo-rl-src - hostPath: - path: /workspace/nemo-rl - type: DirectoryOrCreate diff --git a/infra/kind/SETUP.md b/infra/kind/SETUP.md index 421f2d7b114..273eff4d644 100644 --- a/infra/kind/SETUP.md +++ b/infra/kind/SETUP.md @@ -39,19 +39,14 @@ bash create-cluster.sh cd ../helm helmfile -e kind sync -# 4. Create KAI scheduler queues +# 4. Create KAI scheduler queues + RBAC kubectl apply -f ../examples/kai-queue.yaml +kubectl apply -f ../examples/endpoint-registry-rbac.yaml +kubectl apply -f ../examples/kyverno-kai-policies.yaml -# 5. Test: gang-schedule two GPU pods -kubectl apply -f ../examples/kai_scheduled_pods.yaml -kubectl get pods -w # both go Running at the same time -kubectl logs gpu-test-0 # nvidia-smi output -kubectl delete -f ../examples/kai_scheduled_pods.yaml - -# 6. Test: gang-schedule two RayClusters (each with 1 GPU worker) -kubectl apply -f ../examples/kai_scheduled_rayclusters.yaml -kubectl get rayclusters -w # both become "ready" -kubectl delete -f ../examples/kai_scheduled_rayclusters.yaml +# 5. Deploy disaggregated RL + Gym +kubectl apply -f ../examples/disagg-rayclusters.yaml +kubectl get rayclusters -w # both should become "ready" ``` ## Deploy on a real cluster @@ -59,6 +54,7 @@ kubectl delete -f ../examples/kai_scheduled_rayclusters.yaml ```sh cd infra/helm helmfile -e prod sync +kubectl apply -f infra/examples/kai-queue-prod.yaml ``` This installs the full **GPU Operator** (instead of just the device plugin) along with KAI scheduler and KubeRay. The GPU Operator manages the NVIDIA driver, container toolkit, device plugin, NFD, and DCGM exporter. @@ -72,7 +68,7 @@ Set `driver.enabled=false` in `values/gpu-operator.yaml` if the cluster nodes al | `kind` | nvidia-device-plugin | Local dev — nvkind handles toolkit/runtime | | `prod` | gpu-operator (full) | Real clusters — operator manages everything | -Both environments include KAI scheduler and KubeRay operator. +Both environments include KAI scheduler, KubeRay operator, Kyverno, and Prometheus+Grafana. ## Tear down (kind only) @@ -98,48 +94,35 @@ infra/ │ ├── gpu-operator.yaml # prod only │ ├── kai-scheduler.yaml │ ├── kuberay-operator.yaml -│ ├── kyverno.yaml # Kyverno policy engine -│ └── kube-prometheus-stack.yaml # Prometheus + Grafana -└── examples/ - ├── kai-queue.yaml # KAI queue hierarchy - ├── kai_scheduled_pods.yaml # Gang-scheduled GPU test pods - ├── kai_scheduled_rayclusters.yaml # Gang-scheduled RayClusters - ├── kai_scheduled_sft.yaml # Two gang-scheduled SFT RayJobs - ├── sft_rayjob.yaml # 2-GPU SFT RayJob - ├── raycluster-blocker.yaml # GPU blocker for testing KAI - ├── gym_standalone_config.yaml # Gym standalone server config - ├── disagg_rl_raycluster.yaml # Disagg RL cluster + peer-watcher - ├── disagg_gym_raycluster.yaml # Disagg Gym cluster + peer-watcher - ├── endpoint-registry-rbac.yaml # RBAC for ConfigMap endpoint registry - ├── peer-watcher.py # Sidecar for failure cascading - ├── kai-queue-prod.yaml # 256-GPU prod queue config - ├── kyverno-kai-policies.yaml # Queue enforcement policies - ├── kai-service-monitors.yaml # Prometheus ServiceMonitors for KAI - └── kai-grafana-dashboard.yaml # Grafana dashboard for fairshare +│ ├── kyverno.yaml +│ └── kube-prometheus-stack.yaml +├── examples/ +│ ├── disagg-rayclusters.yaml # Disaggregated RL + Gym (main exemplar) +│ ├── endpoint-registry-rbac.yaml # RBAC for ConfigMap service discovery +│ ├── gym_standalone_config.yaml # Gym standalone server config +│ ├── kai-queue.yaml # 2-GPU kind cluster queues +│ ├── kai-queue-prod.yaml # 288-GPU NVL72 prod queues +│ ├── kyverno-kai-policies.yaml # Queue enforcement policies +│ ├── kai-service-monitors.yaml # Prometheus ServiceMonitors for KAI +│ └── kai-grafana-dashboard.yaml # Grafana fairshare dashboard +└── extensions/ + └── k8s_cli/ # nrl-k8s CLI tool ``` -## Notes - -- **nvkind vs vanilla kind**: nvkind automates GPU device injection, nvidia-container-toolkit installation inside nodes, containerd nvidia runtime configuration, and RuntimeClass registration. -- **nvidia-device-plugin** (kind only): The full GPU Operator fails in kind because its driver validation doesn't work inside kind nodes. The lightweight device plugin with CDI discovery is sufficient since nvkind handles the runtime setup. On a real cluster, use the full GPU Operator (`helmfile -e prod sync`). -- **Device plugin kind overrides**: Affinity is overridden because NFD isn't installed in kind. `runtimeClassName: nvidia` is set so the plugin pod gets NVIDIA libraries injected for NVML discovery. Neither override is needed on a real cluster. -- **KAI scheduler** creates PodGroups automatically for recognized workload types (RayCluster, Job, PyTorchJob, etc.). For bare pods, create a PodGroup manually and annotate pods with `pod-group-name`. -- **RayJob** (not RayCluster) is preferred for batch workloads — it auto-tears down the cluster after the job finishes, avoiding stale Ray state. Requires `submissionMode: HTTPMode` for KAI compatibility. +## Disaggregated RL + Gym -## Failure cascading for disaggregated Gym +The main workload exemplar is `disagg-rayclusters.yaml`: two RayClusters deployed together. -The disaggregated RL/Gym manifests include a **peer-watcher sidecar** on each head pod that monitors the peer cluster via the K8s API. If the peer is deleted, fails, or signals an error via the ConfigMap, the watcher tears down both clusters to release resources. +- **RL cluster** (`raycluster-rl`): Ray head + GPU workers for training (vLLM + Megatron) +- **Gym cluster** (`raycluster-gym`): CPU-only, runs NeMo Gym servers as HTTP service +- **Service discovery**: K8s ConfigMap endpoint registry — RL publishes vLLM URLs, Gym publishes its head server address. Both poll until the peer registers. +- **Failure cascading**: Peer-watcher sidecar (inlined Python script) on each head pod. If either cluster fails or is deleted, both are torn down. -- `peer-watcher.py` — Python sidecar script (deployed as a ConfigMap) -- Monitors: peer RayCluster status + ConfigMap `error` key -- `MAX_PEER_FAILURES` (default 3) consecutive failures before teardown -- Applications can signal errors via `K8sEndpointRegistry.signal_error("message")` +## Notes -Setup: -```sh -kubectl create configmap peer-watcher-script --from-file=peer-watcher.py=infra/examples/peer-watcher.py -kubectl apply -f infra/examples/endpoint-registry-rbac.yaml -``` +- **nvkind vs vanilla kind**: nvkind automates GPU device injection, nvidia-container-toolkit installation inside nodes, containerd nvidia runtime configuration, and RuntimeClass registration. +- **nvidia-device-plugin** (kind only): The full GPU Operator fails in kind because its driver validation doesn't work inside kind nodes. The lightweight device plugin with CDI discovery is sufficient since nvkind handles the runtime setup. +- **KAI scheduler** creates PodGroups automatically for recognized workload types (RayCluster, Job, PyTorchJob, JobSet, etc.). For bare pods, create a PodGroup manually and annotate with `pod-group-name`. ## Fairshare scheduling @@ -167,8 +150,8 @@ KAI distributes GPU resources using hierarchical fair-share with two phases: ### Example configs -- `kai-queue.yaml` — 2-GPU kind cluster (team-a, team-b, equal quotas) -- `kai-queue-prod.yaml` — 256-GPU production cluster (3 departments, 6 teams) +- `kai-queue.yaml` — 2-GPU kind cluster (priority-team + community, imbalanced) +- `kai-queue-prod.yaml` — 288-GPU NVL72 production cluster (priority + community departments) ### Monitoring (Grafana) @@ -185,6 +168,20 @@ Key metrics: `kai_queue_allocated_gpus`, `kai_queue_deserved_gpus`, `kai_e2e_sch RayCluster and RayJob resources must have a `kai.scheduler/queue` label or they're rejected by Kyverno. To enable user→queue access control, uncomment Policy 2 in `kyverno-kai-policies.yaml` and configure the `kai-queue-permissions` ConfigMap. +## CLI (`nrl-k8s`) + +Standalone Python CLI for managing workloads. Install: `pip install -e extensions/k8s_cli` + +```sh +nrl-k8s fairshare # show queue config (quota, limit, weight, priority) +nrl-k8s occupancy # show GPU allocation per node and per queue +nrl-k8s submit JOB_NAME \ # submit gang-scheduled RayJob + --queue priority-team \ + --image nvcr.io/nvidian/nemo-rl:latest \ + --entrypoint "bash tests/functional/sft.sh" \ + --num-gpus 2 +``` + ## TODO: NVL72 topology-aware scheduling KAI v0.14.0 added Ray topology-aware subgroup scheduling ([PR #1125](https://github.com/kai-scheduler/KAI-Scheduler/pull/1125)). Need to test on an actual NVL72 cluster: From 1eb6db625bc504cc278523022c8f986649b98fef Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Fri, 17 Apr 2026 19:26:38 -0700 Subject: [PATCH 08/84] infra: remove k8s CLI, rename queues to high-prio/low-prio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove extensions/k8s_cli/ (not needed for now) - Rename queues: org → root-org, priority-team → high-prio, community → low-prio - Simplify Kyverno policy comments - Update SETUP.md to remove CLI section Signed-off-by: Terry Kong --- extensions/k8s_cli/pyproject.toml | 19 -- extensions/k8s_cli/src/nrl_k8s/__init__.py | 0 extensions/k8s_cli/src/nrl_k8s/cli.py | 149 ---------- extensions/k8s_cli/src/nrl_k8s/k8s_client.py | 278 ------------------- extensions/k8s_cli/tests/__init__.py | 0 extensions/k8s_cli/tests/test_k8s_client.py | 209 -------------- infra/examples/disagg-rayclusters.yaml | 4 +- infra/examples/kai-queue.yaml | 49 ++-- infra/examples/kyverno-kai-policies.yaml | 18 +- infra/kind/SETUP.md | 16 -- 10 files changed, 34 insertions(+), 708 deletions(-) delete mode 100644 extensions/k8s_cli/pyproject.toml delete mode 100644 extensions/k8s_cli/src/nrl_k8s/__init__.py delete mode 100644 extensions/k8s_cli/src/nrl_k8s/cli.py delete mode 100644 extensions/k8s_cli/src/nrl_k8s/k8s_client.py delete mode 100644 extensions/k8s_cli/tests/__init__.py delete mode 100644 extensions/k8s_cli/tests/test_k8s_client.py diff --git a/extensions/k8s_cli/pyproject.toml b/extensions/k8s_cli/pyproject.toml deleted file mode 100644 index b9ef1e20941..00000000000 --- a/extensions/k8s_cli/pyproject.toml +++ /dev/null @@ -1,19 +0,0 @@ -[project] -name = "nrl-k8s" -version = "0.1.0" -description = "CLI for managing nemo-rl workloads on Kubernetes with KAI scheduler" -requires-python = ">=3.10" -dependencies = ["kubernetes>=28.0", "click>=8.0", "rich>=13.0"] - -[project.optional-dependencies] -dev = ["pytest>=8.0", "pytest-mock>=3.0"] - -[project.scripts] -nrl-k8s = "nrl_k8s.cli:main" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["src/nrl_k8s"] diff --git a/extensions/k8s_cli/src/nrl_k8s/__init__.py b/extensions/k8s_cli/src/nrl_k8s/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/extensions/k8s_cli/src/nrl_k8s/cli.py b/extensions/k8s_cli/src/nrl_k8s/cli.py deleted file mode 100644 index 7518c1bb0aa..00000000000 --- a/extensions/k8s_cli/src/nrl_k8s/cli.py +++ /dev/null @@ -1,149 +0,0 @@ -# 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. -"""CLI for managing nemo-rl workloads on Kubernetes with KAI scheduler.""" - -import click -from rich.console import Console -from rich.table import Table - -from nrl_k8s.k8s_client import get_gpu_occupancy, get_queues, submit_gang_rayjob - -console = Console() - - -@click.group() -def main(): - """nrl-k8s: Manage nemo-rl workloads on Kubernetes with KAI scheduler.""" - - -@main.command() -def fairshare(): - """Show KAI scheduler queue fairshare configuration.""" - queues = get_queues() - table = Table(title="KAI Scheduler Queues (Fairshare)") - table.add_column("Queue", style="cyan") - table.add_column("Parent", style="dim") - table.add_column("Priority", justify="right") - table.add_column("GPU Quota", justify="right") - table.add_column("GPU Limit", justify="right") - table.add_column("Weight", justify="right") - table.add_column("Preempt Min", style="dim") - table.add_column("Reclaim Min", style="dim") - - for q in queues: - table.add_row( - q["name"], - q["parent"] or "-", - str(q["priority"]) if q["priority"] else "-", - str(q["gpu_quota"]), - str(q["gpu_limit"]), - str(q["gpu_weight"]), - q["preempt_min_runtime"] or "-", - q["reclaim_min_runtime"] or "-", - ) - - console.print(table) - - -@main.command() -def occupancy(): - """Show current GPU occupancy per node and per queue.""" - data = get_gpu_occupancy() - - # Node table. - node_table = Table(title="GPU Occupancy by Node") - node_table.add_column("Node", style="cyan") - node_table.add_column("Allocatable", justify="right") - node_table.add_column("Allocated", justify="right") - node_table.add_column("Free", justify="right", style="green") - - for n in data["nodes"]: - if n["allocatable"] > 0: - node_table.add_row( - n["name"], - str(n["allocatable"]), - str(n["allocated"]), - str(n["allocatable"] - n["allocated"]), - ) - - node_table.add_section() - node_table.add_row( - "TOTAL", - str(data["total_allocatable"]), - str(data["total_allocated"]), - str(data["total_allocatable"] - data["total_allocated"]), - style="bold", - ) - console.print(node_table) - - # Queue table. - if data["queues"]: - queue_table = Table(title="GPU Occupancy by Queue") - queue_table.add_column("Queue", style="cyan") - queue_table.add_column("Allocated GPUs", justify="right") - for q in data["queues"]: - queue_table.add_row(q["name"], str(q["allocated_gpus"])) - console.print(queue_table) - else: - console.print("[dim]No GPU workloads running.[/dim]") - - -@main.command() -@click.argument("name") -@click.option("--queue", required=True, help="KAI scheduler queue name") -@click.option("--image", required=True, help="Container image") -@click.option("--entrypoint", required=True, help="Entrypoint command") -@click.option("--num-gpus", required=True, type=int, help="Total GPUs requested") -@click.option("--gpus-per-worker", default=1, type=int, help="GPUs per worker pod") -@click.option("--namespace", default="default", help="Kubernetes namespace") -@click.option( - "--segment-size", - default=None, - type=int, - help="Topology segment size (nodes per rack). Creates PodGroup subgroups.", -) -def submit( - name, queue, image, entrypoint, num_gpus, gpus_per_worker, namespace, segment_size -): - """Submit a gang-scheduled RayJob.""" - console.print( - f"Submitting RayJob [cyan]{name}[/cyan] to queue [yellow]{queue}[/yellow]" - ) - console.print( - f" GPUs: {num_gpus} ({num_gpus // gpus_per_worker} workers × {gpus_per_worker} GPU/worker)" - ) - if segment_size: - import math - - num_segments = math.ceil(num_gpus // gpus_per_worker / segment_size) - console.print( - f" Segments: {num_segments} × {segment_size} workers (topology-constrained per rack)" - ) - - result_name = submit_gang_rayjob( - name=name, - queue=queue, - image=image, - entrypoint=entrypoint, - num_gpus=num_gpus, - gpus_per_worker=gpus_per_worker, - namespace=namespace, - segment_size=segment_size, - ) - console.print(f"[green]Created RayJob: {result_name}[/green]") - console.print(f"Watch: kubectl get rayjob {result_name} -w") - - -if __name__ == "__main__": - main() diff --git a/extensions/k8s_cli/src/nrl_k8s/k8s_client.py b/extensions/k8s_cli/src/nrl_k8s/k8s_client.py deleted file mode 100644 index 6bac4861dda..00000000000 --- a/extensions/k8s_cli/src/nrl_k8s/k8s_client.py +++ /dev/null @@ -1,278 +0,0 @@ -# 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. -"""Thin wrapper around the Kubernetes API for KAI scheduler queries.""" - -from __future__ import annotations - -from kubernetes import client, config -from kubernetes.client.exceptions import ApiException - - -def load_k8s_config(): - """Load in-cluster or kubeconfig.""" - try: - config.load_incluster_config() - except config.ConfigException: - config.load_kube_config() - - -def get_queues(namespace: str | None = None) -> list[dict]: - """Fetch all KAI scheduler queues and their resource status.""" - load_k8s_config() - custom = client.CustomObjectsApi() - result = custom.list_cluster_custom_object( - group="scheduling.run.ai", version="v2", plural="queues" - ) - queues = [] - for q in result.get("items", []): - spec = q.get("spec", {}) - gpu = spec.get("resources", {}).get("gpu", {}) - queues.append( - { - "name": q["metadata"]["name"], - "parent": spec.get("parentQueue", ""), - "priority": spec.get("priority", ""), - "gpu_quota": gpu.get("quota", -1), - "gpu_limit": gpu.get("limit", -1), - "gpu_weight": gpu.get("overQuotaWeight", 1), - "preempt_min_runtime": spec.get("preemptMinRuntime", ""), - "reclaim_min_runtime": spec.get("reclaimMinRuntime", ""), - } - ) - return queues - - -def get_gpu_occupancy(namespace: str = "default") -> dict: - """Get current GPU allocation per node and per queue. - - Returns: - { - "nodes": [{"name": ..., "allocatable": N, "allocated": M}], - "queues": [{"name": ..., "allocated_gpus": N}], - "total_allocatable": N, - "total_allocated": M, - } - """ - load_k8s_config() - v1 = client.CoreV1Api() - - # Node-level GPU info. - nodes_info = [] - total_allocatable = 0 - total_allocated = 0 - nodes = v1.list_node() - for node in nodes.items: - alloc = int(node.status.allocatable.get("nvidia.com/gpu", "0")) - total_allocatable += alloc - nodes_info.append( - {"name": node.metadata.name, "allocatable": alloc, "allocated": 0} - ) - - # Count allocated GPUs per node from running pods. - pods = v1.list_pod_for_all_namespaces(field_selector="status.phase=Running") - for pod in pods.items: - node_name = pod.spec.node_name - for container in pod.spec.containers: - limits = container.resources.limits or {} - gpu_req = int(limits.get("nvidia.com/gpu", "0")) - if gpu_req > 0: - total_allocated += gpu_req - for n in nodes_info: - if n["name"] == node_name: - n["allocated"] += gpu_req - - # Queue-level allocation from PodGroups. - queue_alloc: dict[str, int] = {} - for pod in pods.items: - queue = ( - pod.metadata.labels.get("kai.scheduler/queue", "") - if pod.metadata.labels - else "" - ) - if not queue: - continue - for container in pod.spec.containers: - limits = container.resources.limits or {} - gpu_req = int(limits.get("nvidia.com/gpu", "0")) - queue_alloc[queue] = queue_alloc.get(queue, 0) + gpu_req - - return { - "nodes": nodes_info, - "queues": [ - {"name": k, "allocated_gpus": v} for k, v in sorted(queue_alloc.items()) - ], - "total_allocatable": total_allocatable, - "total_allocated": total_allocated, - } - - -def submit_gang_rayjob( - name: str, - queue: str, - image: str, - entrypoint: str, - num_gpus: int, - gpus_per_worker: int = 1, - namespace: str = "default", - segment_size: int | None = None, -) -> str: - """Submit a RayJob with gang scheduling via KAI. - - If segment_size is specified, creates a PodGroup with subgroups for - topology-aware segment scheduling (equivalent to Slurm --segment=N). - - Returns the created RayJob name. - """ - load_k8s_config() - custom = client.CustomObjectsApi() - - num_workers = num_gpus // gpus_per_worker - - rayjob = { - "apiVersion": "ray.io/v1", - "kind": "RayJob", - "metadata": { - "name": name, - "namespace": namespace, - "labels": {"kai.scheduler/queue": queue}, - }, - "spec": { - "entrypoint": entrypoint, - "submissionMode": "HTTPMode", - "shutdownAfterJobFinishes": True, - "ttlSecondsAfterFinished": 60, - "rayClusterSpec": { - "rayVersion": "2.52.0", - "headGroupSpec": { - "rayStartParams": {"object-store-memory": "200000000"}, - "template": { - "spec": { - "schedulerName": "kai-scheduler", - "containers": [ - { - "name": "ray-head", - "image": image, - "resources": { - "requests": {"cpu": "1", "memory": "4Gi"}, - "limits": {"cpu": "4", "memory": "16Gi"}, - }, - "ports": [ - {"containerPort": 6379, "name": "gcs-server"}, - {"containerPort": 8265, "name": "dashboard"}, - ], - "readinessProbe": { - "exec": { - "command": ["ray", "health-check"], - }, - "initialDelaySeconds": 10, - "periodSeconds": 5, - "timeoutSeconds": 5, - }, - } - ], - } - }, - }, - "workerGroupSpecs": [ - { - "groupName": "gpu-workers", - "replicas": num_workers, - "minReplicas": num_workers, - "maxReplicas": num_workers, - "rayStartParams": { - "num-gpus": str(gpus_per_worker), - "object-store-memory": "200000000", - }, - "template": { - "spec": { - "schedulerName": "kai-scheduler", - "containers": [ - { - "name": "ray-worker", - "image": image, - "resources": { - "requests": { - "cpu": "1", - "memory": "4Gi", - "nvidia.com/gpu": str(gpus_per_worker), - }, - "limits": { - "cpu": "8", - "memory": "32Gi", - "nvidia.com/gpu": str(gpus_per_worker), - }, - }, - } - ], - } - }, - } - ], - }, - }, - } - - result = custom.create_namespaced_custom_object( - group="ray.io", - version="v1", - namespace=namespace, - plural="rayjobs", - body=rayjob, - ) - - # If segment_size is specified, create a PodGroup with topology subgroups. - if segment_size and segment_size < num_workers: - import math - - num_segments = math.ceil(num_workers / segment_size) - cluster_name = result["status"].get("rayClusterName", name) - subgroups = [] - for i in range(num_segments): - subgroups.append( - { - "name": f"segment-{i}", - "minMember": min(segment_size, num_workers - i * segment_size), - "topologyConstraint": { - "topology": "cluster-topology", - "requiredTopologyLevel": "topology-rack", - }, - } - ) - - podgroup = { - "apiVersion": "scheduling.run.ai/v2alpha2", - "kind": "PodGroup", - "metadata": { - "name": f"pg-{name}", - "namespace": namespace, - }, - "spec": { - "minMember": num_workers, - "queue": queue, - "subGroups": subgroups, - }, - } - try: - custom.create_namespaced_custom_object( - group="scheduling.run.ai", - version="v2alpha2", - namespace=namespace, - plural="podgroups", - body=podgroup, - ) - except ApiException as e: - if e.status != 409: - raise - - return result["metadata"]["name"] diff --git a/extensions/k8s_cli/tests/__init__.py b/extensions/k8s_cli/tests/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/extensions/k8s_cli/tests/test_k8s_client.py b/extensions/k8s_cli/tests/test_k8s_client.py deleted file mode 100644 index 13d3f9f32a1..00000000000 --- a/extensions/k8s_cli/tests/test_k8s_client.py +++ /dev/null @@ -1,209 +0,0 @@ -# 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. -"""Unit tests for nrl_k8s.k8s_client (mocked K8s API).""" - -from unittest.mock import MagicMock, patch - -import pytest - - -@pytest.fixture(autouse=True) -def mock_k8s_config(): - """Prevent real K8s config loading in all tests.""" - with patch("nrl_k8s.k8s_client.load_k8s_config"): - yield - - -class TestGetQueues: - def test_parses_queue_fields(self): - mock_custom = MagicMock() - mock_custom.list_cluster_custom_object.return_value = { - "items": [ - { - "metadata": {"name": "org"}, - "spec": { - "resources": { - "gpu": {"quota": -1, "limit": -1, "overQuotaWeight": 1} - } - }, - }, - { - "metadata": {"name": "priority-team"}, - "spec": { - "parentQueue": "org", - "priority": 200, - "preemptMinRuntime": "4h", - "reclaimMinRuntime": "15m", - "resources": { - "gpu": {"quota": 1, "limit": 2, "overQuotaWeight": 2} - }, - }, - }, - ] - } - - with patch( - "nrl_k8s.k8s_client.client.CustomObjectsApi", return_value=mock_custom - ): - from nrl_k8s.k8s_client import get_queues - - queues = get_queues() - - assert len(queues) == 2 - assert queues[0]["name"] == "org" - assert queues[0]["gpu_quota"] == -1 - assert queues[1]["name"] == "priority-team" - assert queues[1]["parent"] == "org" - assert queues[1]["priority"] == 200 - assert queues[1]["gpu_quota"] == 1 - assert queues[1]["gpu_limit"] == 2 - assert queues[1]["gpu_weight"] == 2 - assert queues[1]["preempt_min_runtime"] == "4h" - assert queues[1]["reclaim_min_runtime"] == "15m" - - def test_empty_cluster(self): - mock_custom = MagicMock() - mock_custom.list_cluster_custom_object.return_value = {"items": []} - - with patch( - "nrl_k8s.k8s_client.client.CustomObjectsApi", return_value=mock_custom - ): - from nrl_k8s.k8s_client import get_queues - - assert get_queues() == [] - - -class TestGetGpuOccupancy: - def _make_node(self, name, gpu_count): - node = MagicMock() - node.metadata.name = name - node.status.allocatable = {"nvidia.com/gpu": str(gpu_count)} - return node - - def _make_pod(self, name, node_name, gpu_count, queue=""): - pod = MagicMock() - pod.metadata.name = name - pod.spec.node_name = node_name - pod.metadata.labels = {"kai.scheduler/queue": queue} if queue else {} - container = MagicMock() - container.resources.limits = ( - {"nvidia.com/gpu": str(gpu_count)} if gpu_count else {} - ) - pod.spec.containers = [container] - return pod - - def test_counts_gpus(self): - mock_v1 = MagicMock() - nodes = MagicMock() - nodes.items = [self._make_node("worker-0", 2)] - mock_v1.list_node.return_value = nodes - - pods = MagicMock() - pods.items = [ - self._make_pod("pod-a", "worker-0", 1, queue="priority-team"), - self._make_pod("pod-b", "worker-0", 1, queue="community"), - ] - mock_v1.list_pod_for_all_namespaces.return_value = pods - - with patch("nrl_k8s.k8s_client.client.CoreV1Api", return_value=mock_v1): - from nrl_k8s.k8s_client import get_gpu_occupancy - - result = get_gpu_occupancy() - - assert result["total_allocatable"] == 2 - assert result["total_allocated"] == 2 - assert result["nodes"][0]["allocated"] == 2 - assert len(result["queues"]) == 2 - - def test_empty_cluster(self): - mock_v1 = MagicMock() - nodes = MagicMock() - nodes.items = [self._make_node("worker-0", 2)] - mock_v1.list_node.return_value = nodes - pods = MagicMock() - pods.items = [] - mock_v1.list_pod_for_all_namespaces.return_value = pods - - with patch("nrl_k8s.k8s_client.client.CoreV1Api", return_value=mock_v1): - from nrl_k8s.k8s_client import get_gpu_occupancy - - result = get_gpu_occupancy() - - assert result["total_allocated"] == 0 - assert result["queues"] == [] - - -class TestSubmitGangRayjob: - def test_creates_rayjob(self): - mock_custom = MagicMock() - mock_custom.create_namespaced_custom_object.return_value = { - "metadata": {"name": "test-job"}, - "status": {}, - } - - with patch( - "nrl_k8s.k8s_client.client.CustomObjectsApi", return_value=mock_custom - ): - from nrl_k8s.k8s_client import submit_gang_rayjob - - result = submit_gang_rayjob( - name="test-job", - queue="priority-team", - image="rayproject/ray:2.52.0", - entrypoint="echo hello", - num_gpus=2, - ) - - assert result == "test-job" - call_args = mock_custom.create_namespaced_custom_object.call_args - body = call_args.kwargs["body"] - assert body["metadata"]["labels"]["kai.scheduler/queue"] == "priority-team" - assert body["spec"]["submissionMode"] == "HTTPMode" - workers = body["spec"]["rayClusterSpec"]["workerGroupSpecs"][0] - assert workers["replicas"] == 2 - - def test_with_segment_size(self): - mock_custom = MagicMock() - mock_custom.create_namespaced_custom_object.return_value = { - "metadata": {"name": "seg-job"}, - "status": {"rayClusterName": "seg-job-abc"}, - } - - with patch( - "nrl_k8s.k8s_client.client.CustomObjectsApi", return_value=mock_custom - ): - from nrl_k8s.k8s_client import submit_gang_rayjob - - submit_gang_rayjob( - name="seg-job", - queue="priority-team", - image="rayproject/ray:2.52.0", - entrypoint="echo hello", - num_gpus=4, - segment_size=2, - ) - - # Should have 2 calls: RayJob + PodGroup - assert mock_custom.create_namespaced_custom_object.call_count == 2 - pg_call = mock_custom.create_namespaced_custom_object.call_args_list[1] - pg_body = pg_call.kwargs["body"] - assert pg_body["kind"] == "PodGroup" - assert len(pg_body["spec"]["subGroups"]) == 2 - assert pg_body["spec"]["subGroups"][0]["minMember"] == 2 - assert ( - pg_body["spec"]["subGroups"][0]["topologyConstraint"][ - "requiredTopologyLevel" - ] - == "topology-rack" - ) diff --git a/infra/examples/disagg-rayclusters.yaml b/infra/examples/disagg-rayclusters.yaml index 057f4111cb0..c0e1f01ffd9 100644 --- a/infra/examples/disagg-rayclusters.yaml +++ b/infra/examples/disagg-rayclusters.yaml @@ -26,7 +26,7 @@ kind: RayCluster metadata: name: raycluster-rl labels: - kai.scheduler/queue: priority-team # KAI fairshare queue + kai.scheduler/queue: high-prio # KAI fairshare queue spec: rayVersion: "2.52.0" headGroupSpec: @@ -166,7 +166,7 @@ kind: RayCluster metadata: name: raycluster-gym labels: - kai.scheduler/queue: priority-team + kai.scheduler/queue: high-prio spec: rayVersion: "2.52.0" headGroupSpec: diff --git a/infra/examples/kai-queue.yaml b/infra/examples/kai-queue.yaml index 1e8e4f17643..58bf9c8dc05 100644 --- a/infra/examples/kai-queue.yaml +++ b/infra/examples/kai-queue.yaml @@ -1,61 +1,48 @@ # KAI Scheduler queue hierarchy for a 2-GPU kind cluster. -# One priority team and one best-effort team to demonstrate fairshare imbalance. # # Queue hierarchy: # org (root, unlimited) -# ├── priority-team (1 GPU guaranteed, burst to 2, higher priority + weight) -# └── community (0 GPU guaranteed, uses idle GPUs only, lower priority) +# ├── high-prio (1 GPU guaranteed, burst to 2, priority 200, weight 2) +# └── low-prio (0 GPU guaranteed, burst to 2, priority 50, weight 1) # -# Behavior: -# - priority-team always gets its 1 GPU quota, and gets surplus first (priority 200). -# - community has no guarantee — it runs on whatever priority-team isn't using. -# - If both compete, priority-team gets 2x the surplus (overQuotaWeight 2 vs 1). -# - community jobs can be reclaimed after 15m if priority-team needs resources back. -# - priority-team jobs are protected from preemption for 4 hours. +# high-prio: gets its guaranteed GPU first, gets surplus before low-prio, +# and is reclaimed last. Jobs protected from preemption for 4 hours. +# low-prio: best-effort — uses idle GPUs, first to be reclaimed. apiVersion: scheduling.run.ai/v2 kind: Queue metadata: - name: org + name: root-org spec: resources: gpu: { quota: -1, limit: -1, overQuotaWeight: 1 } cpu: { quota: -1, limit: -1, overQuotaWeight: 1 } memory: { quota: -1, limit: -1, overQuotaWeight: 1 } --- -# Priority team — guaranteed resources, high priority, protected from preemption. apiVersion: scheduling.run.ai/v2 kind: Queue metadata: - name: priority-team + name: high-prio spec: - parentQueue: org - priority: 200 # served first for over-quota, reclaimed last - preemptMinRuntime: "4h" # 4 hours before a higher-priority queue can preempt - reclaimMinRuntime: "15m" # 15 min grace before over-quota resources reclaimed + parentQueue: root-org + priority: 200 + preemptMinRuntime: "4h" + reclaimMinRuntime: "15m" resources: - gpu: - quota: 1 # guaranteed 1 GPU - limit: 2 # can burst to full cluster when idle - overQuotaWeight: 2 # gets 2x surplus share vs community + gpu: { quota: 1, limit: 2, overQuotaWeight: 2 } cpu: { quota: -1, limit: -1, overQuotaWeight: 2 } memory: { quota: -1, limit: -1, overQuotaWeight: 2 } --- -# Community — best-effort, no guarantee, uses idle resources. -# First to be reclaimed when priority-team needs resources back. apiVersion: scheduling.run.ai/v2 kind: Queue metadata: - name: community + name: low-prio spec: - parentQueue: org - priority: 50 # lowest priority — served last, reclaimed first - preemptMinRuntime: "4h" # still protect running jobs for 4 hours - reclaimMinRuntime: "15m" # 15 min grace (same as priority-team for fairness) + parentQueue: root-org + priority: 50 + preemptMinRuntime: "4h" + reclaimMinRuntime: "15m" resources: - gpu: - quota: 0 # no guaranteed GPUs - limit: 2 # can use the whole cluster if nobody else needs it - overQuotaWeight: 1 # half the surplus weight of priority-team + gpu: { quota: 0, limit: 2, overQuotaWeight: 1 } cpu: { quota: -1, limit: -1, overQuotaWeight: 1 } memory: { quota: -1, limit: -1, overQuotaWeight: 1 } diff --git a/infra/examples/kyverno-kai-policies.yaml b/infra/examples/kyverno-kai-policies.yaml index f500edc2922..26e1f496b1b 100644 --- a/infra/examples/kyverno-kai-policies.yaml +++ b/infra/examples/kyverno-kai-policies.yaml @@ -1,14 +1,24 @@ # Kyverno policies for KAI scheduler queue enforcement. # # Policy 1: RayCluster and RayJob must have a kai.scheduler/queue label. +# Only checks that the label exists and is non-empty — does NOT validate +# that the queue name is valid or that the user is allowed to use it. # Validates at the CRD level (not Pod level) because the KubeRay operator # creates pods — pod-level validation would check the operator's identity, # not the actual user who submitted the workload. # -# Policy 2 (optional): Validate that the queue is allowed for the requesting user. -# Uses a ConfigMap (kai-queue-permissions) for user→queue mapping. -# The user identity comes from the K8s AdmissionReview userInfo (OIDC email, -# certificate CN, or ServiceAccount name). +# To extend this with more advanced validation, you could: +# - Validate the queue exists: add a context lookup against the K8s API for +# the Queue CRD and deny if not found. +# - Restrict queues per user: uncomment Policy 2 below, which uses a ConfigMap +# to map K8s usernames to allowed queue names. +# - Restrict queues per namespace: add a rule matching on request.namespace +# and comparing against an allowed list per namespace. +# +# Policy 2 (optional, commented out): Validate that the queue is allowed for +# the requesting user. Uses a ConfigMap (kai-queue-permissions) for user→queue +# mapping. The user identity comes from the K8s AdmissionReview userInfo +# (OIDC email, certificate CN, or ServiceAccount name). # # Prerequisites: # helm install kyverno kyverno/kyverno -n kyverno --create-namespace diff --git a/infra/kind/SETUP.md b/infra/kind/SETUP.md index 273eff4d644..90804be3487 100644 --- a/infra/kind/SETUP.md +++ b/infra/kind/SETUP.md @@ -105,8 +105,6 @@ infra/ │ ├── kyverno-kai-policies.yaml # Queue enforcement policies │ ├── kai-service-monitors.yaml # Prometheus ServiceMonitors for KAI │ └── kai-grafana-dashboard.yaml # Grafana fairshare dashboard -└── extensions/ - └── k8s_cli/ # nrl-k8s CLI tool ``` ## Disaggregated RL + Gym @@ -168,20 +166,6 @@ Key metrics: `kai_queue_allocated_gpus`, `kai_queue_deserved_gpus`, `kai_e2e_sch RayCluster and RayJob resources must have a `kai.scheduler/queue` label or they're rejected by Kyverno. To enable user→queue access control, uncomment Policy 2 in `kyverno-kai-policies.yaml` and configure the `kai-queue-permissions` ConfigMap. -## CLI (`nrl-k8s`) - -Standalone Python CLI for managing workloads. Install: `pip install -e extensions/k8s_cli` - -```sh -nrl-k8s fairshare # show queue config (quota, limit, weight, priority) -nrl-k8s occupancy # show GPU allocation per node and per queue -nrl-k8s submit JOB_NAME \ # submit gang-scheduled RayJob - --queue priority-team \ - --image nvcr.io/nvidian/nemo-rl:latest \ - --entrypoint "bash tests/functional/sft.sh" \ - --num-gpus 2 -``` - ## TODO: NVL72 topology-aware scheduling KAI v0.14.0 added Ray topology-aware subgroup scheduling ([PR #1125](https://github.com/kai-scheduler/KAI-Scheduler/pull/1125)). Need to test on an actual NVL72 cluster: From f0ec7fa11a7e1e1a2975fe1e4603808a361c57e1 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Fri, 17 Apr 2026 22:23:42 -0700 Subject: [PATCH 09/84] infra: remove Kyverno MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not needed right now — queue enforcement can be added back later if required. Signed-off-by: Terry Kong --- infra/examples/kyverno-kai-policies.yaml | 116 ----------------------- infra/helm/helmfile.yaml | 15 --- infra/helm/values/kyverno.yaml | 10 -- infra/kind/SETUP.md | 10 +- 4 files changed, 1 insertion(+), 150 deletions(-) delete mode 100644 infra/examples/kyverno-kai-policies.yaml delete mode 100644 infra/helm/values/kyverno.yaml diff --git a/infra/examples/kyverno-kai-policies.yaml b/infra/examples/kyverno-kai-policies.yaml deleted file mode 100644 index 26e1f496b1b..00000000000 --- a/infra/examples/kyverno-kai-policies.yaml +++ /dev/null @@ -1,116 +0,0 @@ -# Kyverno policies for KAI scheduler queue enforcement. -# -# Policy 1: RayCluster and RayJob must have a kai.scheduler/queue label. -# Only checks that the label exists and is non-empty — does NOT validate -# that the queue name is valid or that the user is allowed to use it. -# Validates at the CRD level (not Pod level) because the KubeRay operator -# creates pods — pod-level validation would check the operator's identity, -# not the actual user who submitted the workload. -# -# To extend this with more advanced validation, you could: -# - Validate the queue exists: add a context lookup against the K8s API for -# the Queue CRD and deny if not found. -# - Restrict queues per user: uncomment Policy 2 below, which uses a ConfigMap -# to map K8s usernames to allowed queue names. -# - Restrict queues per namespace: add a rule matching on request.namespace -# and comparing against an allowed list per namespace. -# -# Policy 2 (optional, commented out): Validate that the queue is allowed for -# the requesting user. Uses a ConfigMap (kai-queue-permissions) for user→queue -# mapping. The user identity comes from the K8s AdmissionReview userInfo -# (OIDC email, certificate CN, or ServiceAccount name). -# -# Prerequisites: -# helm install kyverno kyverno/kyverno -n kyverno --create-namespace - -# --- Policy 1: Require queue label --- -apiVersion: kyverno.io/v1 -kind: ClusterPolicy -metadata: - name: require-kai-queue-label - annotations: - policies.kyverno.io/title: Require KAI queue label on Ray workloads - policies.kyverno.io/description: >- - RayCluster and RayJob resources must specify a kai.scheduler/queue label - so the KAI scheduler can assign them to the correct fairshare queue. -spec: - validationFailureAction: Enforce - background: true - rules: - - name: require-kai-queue-on-raycluster - match: - any: - - resources: - kinds: - - ray.io/v1/RayCluster - validate: - message: >- - RayCluster "{{request.object.metadata.name}}" must have a - 'kai.scheduler/queue' label. Add it to metadata.labels. - pattern: - metadata: - labels: - kai.scheduler/queue: "?*" - - name: require-kai-queue-on-rayjob - match: - any: - - resources: - kinds: - - ray.io/v1/RayJob - validate: - message: >- - RayJob "{{request.object.metadata.name}}" must have a - 'kai.scheduler/queue' label. Add it to metadata.labels. - pattern: - metadata: - labels: - kai.scheduler/queue: "?*" - ---- -# --- Policy 2: Validate user is allowed to use the queue (optional) --- -# Uncomment and configure the ConfigMap below to enable. -# The ConfigMap maps K8s usernames to comma-separated allowed queues. -# -# apiVersion: v1 -# kind: ConfigMap -# metadata: -# name: kai-queue-permissions -# namespace: kyverno -# data: -# # K8s username → allowed queues (comma-separated) -# alice@company.com: "team-a,team-b" -# bob@company.com: "team-b" -# # ServiceAccounts use the format: system:serviceaccount:namespace:name -# system:serviceaccount:default:nemo-rl-endpoint-registry: "team-a,team-b" -# --- -# apiVersion: kyverno.io/v1 -# kind: ClusterPolicy -# metadata: -# name: validate-kai-queue-permission -# spec: -# validationFailureAction: Enforce -# background: false -# rules: -# - name: check-queue-permission -# match: -# any: -# - resources: -# kinds: -# - ray.io/v1/RayCluster -# - ray.io/v1/RayJob -# context: -# - name: permissions -# configMap: -# name: kai-queue-permissions -# namespace: kyverno -# validate: -# message: >- -# User "{{request.userInfo.username}}" is not allowed to use queue -# "{{request.object.metadata.labels."kai.scheduler/queue"}}". -# Allowed queues: {{permissions.data[request.userInfo.username] || 'none'}} -# deny: -# conditions: -# all: -# - key: "{{request.object.metadata.labels.\"kai.scheduler/queue\"}}" -# operator: AnyNotIn -# value: "{{permissions.data[request.userInfo.username] || '' | split(@, ',')}}" diff --git a/infra/helm/helmfile.yaml b/infra/helm/helmfile.yaml index 0ac8858b6e0..ae899e8e4ae 100644 --- a/infra/helm/helmfile.yaml +++ b/infra/helm/helmfile.yaml @@ -17,8 +17,6 @@ repositories: url: https://ray-project.github.io/kuberay-helm/ - name: prometheus-community url: https://prometheus-community.github.io/helm-charts -- name: kyverno - url: https://kyverno.github.io/kyverno/ releases: # @@ -71,19 +69,6 @@ releases: values: - values/kuberay-operator.yaml -# -# Kyverno: policy engine for enforcing queue labels on Ray workloads. -# Policies are applied separately: kubectl apply -f kyverno-kai-policies.yaml -# -- name: kyverno - namespace: kyverno - createNamespace: true - chart: kyverno/kyverno - version: 3.7.1 - wait: true - values: - - values/kyverno.yaml - # # Prometheus + Grafana: fairshare monitoring and dashboards. # Access Grafana: kubectl port-forward svc/kube-prometheus-stack-grafana -n monitoring 3000:80 diff --git a/infra/helm/values/kyverno.yaml b/infra/helm/values/kyverno.yaml deleted file mode 100644 index 3c9f7d5099a..00000000000 --- a/infra/helm/values/kyverno.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# Kyverno policy engine — enforces queue labels on Ray workloads. -# Policies are applied separately via: kubectl apply -f kyverno-kai-policies.yaml -admissionController: - replicas: 1 -backgroundController: - replicas: 1 -cleanupController: - replicas: 1 -reportsController: - replicas: 1 diff --git a/infra/kind/SETUP.md b/infra/kind/SETUP.md index 90804be3487..a92138391fa 100644 --- a/infra/kind/SETUP.md +++ b/infra/kind/SETUP.md @@ -42,7 +42,6 @@ helmfile -e kind sync # 4. Create KAI scheduler queues + RBAC kubectl apply -f ../examples/kai-queue.yaml kubectl apply -f ../examples/endpoint-registry-rbac.yaml -kubectl apply -f ../examples/kyverno-kai-policies.yaml # 5. Deploy disaggregated RL + Gym kubectl apply -f ../examples/disagg-rayclusters.yaml @@ -68,7 +67,7 @@ Set `driver.enabled=false` in `values/gpu-operator.yaml` if the cluster nodes al | `kind` | nvidia-device-plugin | Local dev — nvkind handles toolkit/runtime | | `prod` | gpu-operator (full) | Real clusters — operator manages everything | -Both environments include KAI scheduler, KubeRay operator, Kyverno, and Prometheus+Grafana. +Both environments include KAI scheduler, KubeRay operator, and Prometheus+Grafana. ## Tear down (kind only) @@ -94,7 +93,6 @@ infra/ │ ├── gpu-operator.yaml # prod only │ ├── kai-scheduler.yaml │ ├── kuberay-operator.yaml -│ ├── kyverno.yaml │ └── kube-prometheus-stack.yaml ├── examples/ │ ├── disagg-rayclusters.yaml # Disaggregated RL + Gym (main exemplar) @@ -102,7 +100,6 @@ infra/ │ ├── gym_standalone_config.yaml # Gym standalone server config │ ├── kai-queue.yaml # 2-GPU kind cluster queues │ ├── kai-queue-prod.yaml # 288-GPU NVL72 prod queues -│ ├── kyverno-kai-policies.yaml # Queue enforcement policies │ ├── kai-service-monitors.yaml # Prometheus ServiceMonitors for KAI │ └── kai-grafana-dashboard.yaml # Grafana fairshare dashboard ``` @@ -162,17 +159,12 @@ kubectl port-forward svc/kube-prometheus-stack-grafana -n monitoring 3000:80 Key metrics: `kai_queue_allocated_gpus`, `kai_queue_deserved_gpus`, `kai_e2e_scheduling_latency_milliseconds`. -### Kyverno queue enforcement - -RayCluster and RayJob resources must have a `kai.scheduler/queue` label or they're rejected by Kyverno. To enable user→queue access control, uncomment Policy 2 in `kyverno-kai-policies.yaml` and configure the `kai-queue-permissions` ConfigMap. - ## TODO: NVL72 topology-aware scheduling KAI v0.14.0 added Ray topology-aware subgroup scheduling ([PR #1125](https://github.com/kai-scheduler/KAI-Scheduler/pull/1125)). Need to test on an actual NVL72 cluster: - **Confirm `--segment=N` equivalent works**: KAI's `subGroups` with per-subgroup `topologyConstraint.requiredTopologyLevel: "rack"` should be the equivalent of Slurm's `--segment=N`. Each subgroup of N nodes is constrained to one rack. Unclear if this works correctly for cross-rack scheduling (e.g., `--segment=16` with 32 total nodes = 2 racks). - **Auto-segmentation not yet implemented**: The design doc at [`docs/developer/designs/segmented-subgroups/`](https://github.com/kai-scheduler/KAI-Scheduler/blob/main/docs/developer/designs/segmented-subgroups/README.md) proposes `kai.scheduler/segment-size` annotation for automatic subgroup creation, but it depends on "Replica-Type SubGrouping" which isn't shipped yet. See [Issue #1189](https://github.com/kai-scheduler/KAI-Scheduler/issues/1189) and [PR #1127](https://github.com/kai-scheduler/KAI-Scheduler/pull/1127) (minSubGroup field, still open). -- **Test with our k8s CLI**: The `nrl-k8s submit` command (see `extensions/k8s_cli/`) should support `--segment-size` that auto-generates the PodGroup subgroups until KAI ships native support. ## TODO: Log persistence From f2b003a9f7cc94eddcc2ea927ca1859b4087b2ff Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Fri, 17 Apr 2026 22:58:13 -0700 Subject: [PATCH 10/84] infra: remove KAI Grafana dashboard Signed-off-by: Terry Kong --- infra/examples/kai-grafana-dashboard.yaml | 117 ---------------------- infra/kind/SETUP.md | 3 +- 2 files changed, 1 insertion(+), 119 deletions(-) delete mode 100644 infra/examples/kai-grafana-dashboard.yaml diff --git a/infra/examples/kai-grafana-dashboard.yaml b/infra/examples/kai-grafana-dashboard.yaml deleted file mode 100644 index 3d3a1ae88c5..00000000000 --- a/infra/examples/kai-grafana-dashboard.yaml +++ /dev/null @@ -1,117 +0,0 @@ -# Grafana dashboard for KAI scheduler fairshare monitoring. -# Deployed as a ConfigMap — Grafana's sidecar auto-discovers it. -# -# Panels: -# 1. GPU Allocation vs Fair Share per Queue (bar chart) -# 2. GPU Allocation Over Time (time series) -# 3. Preemption Events (counter) -# 4. Scheduling Latency (gauge) -apiVersion: v1 -kind: ConfigMap -metadata: - name: kai-fairshare-dashboard - namespace: monitoring - labels: - grafana_dashboard: "1" -data: - kai-fairshare.json: | - { - "annotations": { "list": [] }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 1, - "links": [], - "panels": [ - { - "title": "GPU Allocation vs Fair Share per Queue", - "description": "Current GPU allocation (solid) vs computed fair share (dashed) for each queue. Queues above their fair share may have resources reclaimed.", - "type": "bargauge", - "gridPos": { "h": 8, "w": 24, "x": 0, "y": 0 }, - "targets": [ - { - "expr": "kai_queue_allocated_gpus", - "legendFormat": "{{queue_name}} allocated", - "refId": "A" - }, - { - "expr": "kai_queue_deserved_gpus", - "legendFormat": "{{queue_name}} deserved (fair share)", - "refId": "B" - } - ], - "fieldConfig": { - "defaults": { - "unit": "short", - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } - } - } - }, - { - "title": "GPU Allocation Over Time", - "description": "How GPU allocation per queue changes over time. Useful for observing fairshare oscillation and reclaim events.", - "type": "timeseries", - "gridPos": { "h": 10, "w": 24, "x": 0, "y": 8 }, - "targets": [ - { - "expr": "kai_queue_allocated_gpus", - "legendFormat": "{{queue_name}} allocated", - "refId": "A" - }, - { - "expr": "kai_queue_deserved_gpus", - "legendFormat": "{{queue_name}} fair share", - "refId": "B" - } - ], - "fieldConfig": { - "defaults": { - "unit": "short", - "custom": { "lineWidth": 2, "fillOpacity": 10 } - } - } - }, - { - "title": "Preemption & Eviction Events", - "description": "Count of preemption attempts and pod evictions. Spikes indicate resource contention between queues.", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 18 }, - "targets": [ - { - "expr": "rate(kai_total_preemption_attempts[5m])", - "legendFormat": "preemption attempts/s", - "refId": "A" - }, - { - "expr": "rate(kai_pod_group_evicted_pods_total[5m])", - "legendFormat": "evictions/s ({{queue_name}})", - "refId": "B" - } - ], - "fieldConfig": { - "defaults": { "unit": "ops", "custom": { "lineWidth": 2 } } - } - }, - { - "title": "Scheduling Latency", - "description": "End-to-end scheduling cycle latency. High latency may indicate resource fragmentation or too many pending workloads.", - "type": "timeseries", - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 18 }, - "targets": [ - { - "expr": "kai_e2e_scheduling_latency_milliseconds", - "legendFormat": "e2e latency (ms)", - "refId": "A" - } - ], - "fieldConfig": { - "defaults": { "unit": "ms", "custom": { "lineWidth": 2 } } - } - } - ], - "schemaVersion": 39, - "tags": ["kai", "fairshare", "gpu"], - "templating": { "list": [] }, - "time": { "from": "now-1h", "to": "now" }, - "title": "KAI Scheduler Fairshare", - "uid": "kai-fairshare" - } diff --git a/infra/kind/SETUP.md b/infra/kind/SETUP.md index a92138391fa..76440302674 100644 --- a/infra/kind/SETUP.md +++ b/infra/kind/SETUP.md @@ -100,8 +100,7 @@ infra/ │ ├── gym_standalone_config.yaml # Gym standalone server config │ ├── kai-queue.yaml # 2-GPU kind cluster queues │ ├── kai-queue-prod.yaml # 288-GPU NVL72 prod queues -│ ├── kai-service-monitors.yaml # Prometheus ServiceMonitors for KAI -│ └── kai-grafana-dashboard.yaml # Grafana fairshare dashboard +│ └── kai-service-monitors.yaml # Prometheus ServiceMonitors for KAI ``` ## Disaggregated RL + Gym From 7b3cb8663fc55c617b77bfd1afc77d6db968e038 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Fri, 17 Apr 2026 23:00:02 -0700 Subject: [PATCH 11/84] infra: remove Prometheus+Grafana stack Signed-off-by: Terry Kong --- infra/examples/kai-service-monitors.yaml | 53 -------------------- infra/helm/helmfile.yaml | 14 ------ infra/helm/values/kai-scheduler.yaml | 7 --- infra/helm/values/kube-prometheus-stack.yaml | 46 ----------------- infra/kind/SETUP.md | 19 ++----- 5 files changed, 3 insertions(+), 136 deletions(-) delete mode 100644 infra/examples/kai-service-monitors.yaml delete mode 100644 infra/helm/values/kube-prometheus-stack.yaml diff --git a/infra/examples/kai-service-monitors.yaml b/infra/examples/kai-service-monitors.yaml deleted file mode 100644 index 3ee42c8fab9..00000000000 --- a/infra/examples/kai-service-monitors.yaml +++ /dev/null @@ -1,53 +0,0 @@ -# ServiceMonitors for KAI scheduler components. -# These tell Prometheus to scrape metrics from the scheduler, binder, and queue-controller. -# -# Prerequisites: kube-prometheus-stack must be installed (provides Prometheus Operator). -# Apply after: helmfile sync - -# Scheduler metrics: scheduling latency, fairshare allocation, preemption counts. -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: kai-scheduler - namespace: kai-scheduler - labels: - app: kai-scheduler -spec: - selector: - matchLabels: - app: kai-scheduler-default - endpoints: - - port: metrics - interval: 15s ---- -# Binder metrics: bind latency, bind success/failure rates. -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: kai-binder - namespace: kai-scheduler - labels: - app: kai-binder -spec: - selector: - matchLabels: - app: binder - endpoints: - - port: metrics - interval: 15s ---- -# Queue controller metrics: queue allocation, deserved GPUs, fair share. -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: kai-queue-controller - namespace: kai-scheduler - labels: - app: kai-queue-controller -spec: - selector: - matchLabels: - app: queue-controller - endpoints: - - port: metrics - interval: 15s diff --git a/infra/helm/helmfile.yaml b/infra/helm/helmfile.yaml index ae899e8e4ae..8175c8833e4 100644 --- a/infra/helm/helmfile.yaml +++ b/infra/helm/helmfile.yaml @@ -15,8 +15,6 @@ repositories: url: https://helm.ngc.nvidia.com/nvidia - name: kuberay url: https://ray-project.github.io/kuberay-helm/ -- name: prometheus-community - url: https://prometheus-community.github.io/helm-charts releases: # @@ -69,15 +67,3 @@ releases: values: - values/kuberay-operator.yaml -# -# Prometheus + Grafana: fairshare monitoring and dashboards. -# Access Grafana: kubectl port-forward svc/kube-prometheus-stack-grafana -n monitoring 3000:80 -# -- name: kube-prometheus-stack - namespace: monitoring - createNamespace: true - chart: prometheus-community/kube-prometheus-stack - wait: true - waitForJobs: true - values: - - values/kube-prometheus-stack.yaml diff --git a/infra/helm/values/kai-scheduler.yaml b/infra/helm/values/kai-scheduler.yaml index 3e50318b6ca..b1ef917c182 100644 --- a/infra/helm/values/kai-scheduler.yaml +++ b/infra/helm/values/kai-scheduler.yaml @@ -1,10 +1,3 @@ # KAI Scheduler configuration. -# Creates a default queue and enables Prometheus metrics for fairshare monitoring. defaultQueue: createDefaultQueue: true - -# Enable Prometheus metrics endpoint on KAI components. -# Metrics are scraped by the kube-prometheus-stack ServiceMonitors. -global: - prometheus: - enabled: true diff --git a/infra/helm/values/kube-prometheus-stack.yaml b/infra/helm/values/kube-prometheus-stack.yaml deleted file mode 100644 index 6e05689900d..00000000000 --- a/infra/helm/values/kube-prometheus-stack.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Prometheus + Grafana for KAI scheduler fairshare monitoring. -# -# Access Grafana: kubectl port-forward svc/kube-prometheus-stack-grafana -n monitoring 3000:80 -# Default credentials: admin / prom-operator - -prometheus: - prometheusSpec: - # Scrape KAI metrics from the kai-scheduler namespace. - serviceMonitorSelectorNilUsesHelmValues: false - serviceMonitorNamespaceSelector: {} - serviceMonitorSelector: {} - # Reduce resource usage for kind dev cluster. - resources: - requests: - cpu: 200m - memory: 512Mi - limits: - memory: 2Gi - retention: 7d - storageSpec: - volumeClaimTemplate: - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 5Gi - -grafana: - enabled: true - adminPassword: prom-operator - persistence: - enabled: false - sidecar: - dashboards: - enabled: true # auto-discovers ConfigMaps with grafana_dashboard: "1" - searchNamespace: ALL - datasources: - enabled: true - -# Disable components we don't need on a kind dev cluster. -alertmanager: - enabled: false -nodeExporter: - enabled: false -kubeStateMetrics: - enabled: false diff --git a/infra/kind/SETUP.md b/infra/kind/SETUP.md index 76440302674..9301fd82505 100644 --- a/infra/kind/SETUP.md +++ b/infra/kind/SETUP.md @@ -67,7 +67,7 @@ Set `driver.enabled=false` in `values/gpu-operator.yaml` if the cluster nodes al | `kind` | nvidia-device-plugin | Local dev — nvkind handles toolkit/runtime | | `prod` | gpu-operator (full) | Real clusters — operator manages everything | -Both environments include KAI scheduler, KubeRay operator, and Prometheus+Grafana. +Both environments include KAI scheduler and KubeRay operator. ## Tear down (kind only) @@ -92,15 +92,13 @@ infra/ │ ├── nvidia-device-plugin.yaml # kind only │ ├── gpu-operator.yaml # prod only │ ├── kai-scheduler.yaml -│ ├── kuberay-operator.yaml -│ └── kube-prometheus-stack.yaml +│ └── kuberay-operator.yaml ├── examples/ │ ├── disagg-rayclusters.yaml # Disaggregated RL + Gym (main exemplar) │ ├── endpoint-registry-rbac.yaml # RBAC for ConfigMap service discovery │ ├── gym_standalone_config.yaml # Gym standalone server config │ ├── kai-queue.yaml # 2-GPU kind cluster queues -│ ├── kai-queue-prod.yaml # 288-GPU NVL72 prod queues -│ └── kai-service-monitors.yaml # Prometheus ServiceMonitors for KAI +│ └── kai-queue-prod.yaml # 288-GPU NVL72 prod queues ``` ## Disaggregated RL + Gym @@ -147,17 +145,6 @@ KAI distributes GPU resources using hierarchical fair-share with two phases: - `kai-queue.yaml` — 2-GPU kind cluster (priority-team + community, imbalanced) - `kai-queue-prod.yaml` — 288-GPU NVL72 production cluster (priority + community departments) -### Monitoring (Grafana) - -```sh -kubectl port-forward svc/kube-prometheus-stack-grafana -n monitoring 3000:80 -# Open http://localhost:3000 -# Login: admin / prom-operator -# Dashboard: search "KAI Scheduler Fairshare" or go to http://localhost:3000/d/kai-fairshare -``` - -Key metrics: `kai_queue_allocated_gpus`, `kai_queue_deserved_gpus`, `kai_e2e_scheduling_latency_milliseconds`. - ## TODO: NVL72 topology-aware scheduling KAI v0.14.0 added Ray topology-aware subgroup scheduling ([PR #1125](https://github.com/kai-scheduler/KAI-Scheduler/pull/1125)). Need to test on an actual NVL72 cluster: From 23d2c7deeea1af99e3010fa8c3dc62ecea2ea2d6 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 19:30:51 -0700 Subject: [PATCH 12/84] infra: move standalone gym server from Gym submodule to nemo-rl (infra part) Signed-off-by: Terry Kong --- infra/examples/disagg-rayclusters.yaml | 4 ++-- infra/examples/gym_standalone_config.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/infra/examples/disagg-rayclusters.yaml b/infra/examples/disagg-rayclusters.yaml index c0e1f01ffd9..68cd7c9d13d 100644 --- a/infra/examples/disagg-rayclusters.yaml +++ b/infra/examples/disagg-rayclusters.yaml @@ -16,7 +16,7 @@ # Usage: # kubectl apply -f disagg-rayclusters.yaml # # On RL head: run GRPO with +env.disagg_job_id= -# # On Gym head: run standalone_server.py with --job-id= +# # On Gym head: uv run --extra nemo_gym python -m nemo_rl.distributed.standalone_gym_server --job-id= # ======================== # RL RayCluster (GPU) @@ -179,7 +179,7 @@ spec: imagePullSecrets: - name: nvcr-secret containers: - # --- Gym Ray head (runs standalone_server.py) --- + # --- Gym Ray head (runs standalone_gym_server) --- - name: ray-head image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 resources: diff --git a/infra/examples/gym_standalone_config.yaml b/infra/examples/gym_standalone_config.yaml index 6cd45c7b544..2fc223a5888 100644 --- a/infra/examples/gym_standalone_config.yaml +++ b/infra/examples/gym_standalone_config.yaml @@ -1,5 +1,5 @@ # Gym standalone server config (extracted from env.nemo_gym section). -# Used with: python -m nemo_gym.standalone_server --config-yaml this_file.yaml +# Used with: uv run --extra nemo_gym python -m nemo_rl.distributed.standalone_gym_server --config-yaml this_file.yaml config_paths: - responses_api_models/vllm_model/configs/vllm_model_for_training.yaml - resources_servers/workplace_assistant/configs/workplace_assistant.yaml From c15c0e6d36d217802db45a4487496bc34e151a31 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Fri, 17 Apr 2026 23:50:33 -0700 Subject: [PATCH 13/84] infra: add monolithic RayJob and disagg JobSet examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three workload deployment patterns: 1. rayjob-monolithic.yaml — single-cluster RayJob (1 GPU, KubeRay) 2. disagg-rayclusters.yaml — two KubeRay RayClusters + peer-watcher 3. disagg-jobset.yaml — single JobSet with native failure/startup policies Also adds JobSet controller (v0.11.1) to the helmfile. Signed-off-by: Terry Kong --- infra/examples/disagg-jobset.yaml | 263 ++++++++++++++++++++++++++ infra/examples/rayjob-monolithic.yaml | 83 ++++++++ infra/helm/helmfile.yaml | 10 + infra/kind/SETUP.md | 33 +++- 4 files changed, 380 insertions(+), 9 deletions(-) create mode 100644 infra/examples/disagg-jobset.yaml create mode 100644 infra/examples/rayjob-monolithic.yaml diff --git a/infra/examples/disagg-jobset.yaml b/infra/examples/disagg-jobset.yaml new file mode 100644 index 00000000000..aa72cbd4dea --- /dev/null +++ b/infra/examples/disagg-jobset.yaml @@ -0,0 +1,263 @@ +# Disaggregated RL + Gym as a single JobSet (alternative to disagg-rayclusters.yaml). +# +# Four ReplicatedJobs: +# rl-head — Ray head daemon (CPU), long-running +# rl-workers — GPU workers, connect to rl-head +# gym — Standalone Gym HTTP server (CPU) +# driver — Submits GRPO training, gates JobSet success +# +# Advantages over the KubeRay RayCluster approach: +# - Native failure cascading via failurePolicy (no peer-watcher sidecar) +# - Startup ordering via dependsOn (no init container polling) +# - Predictable DNS names (no Service or ConfigMap needed for discovery) +# - Single resource to manage (one JobSet instead of two RayClusters + RBAC) +# +# DNS names (auto-created by JobSet headless service): +# rl-head: disagg-job-rl-head-0-0.disagg-job +# rl-workers: disagg-job-rl-workers-0-0.disagg-job (one pod per worker) +# gym: disagg-job-gym-0-0.disagg-job +# driver: disagg-job-driver-0-0.disagg-job +# +# Prerequisites: +# JobSet controller installed (helmfile sync installs it) +# kubectl apply -f kai-queue.yaml +# kubectl apply -f endpoint-registry-rbac.yaml +# +# Usage: +# kubectl apply -f disagg-jobset.yaml +# kubectl get jobset disagg-job -w + +apiVersion: jobset.x-k8s.io/v1alpha2 +kind: JobSet +metadata: + name: disagg-job + labels: + kai.scheduler/queue: high-prio +spec: + network: + enableDNSHostnames: true + publishNotReadyAddresses: true + + # Head pod address propagated to all pods as a label. + coordinator: + replicatedJob: rl-head + jobIndex: 0 + podIndex: 0 + + # JobSet succeeds when the driver exits 0. + successPolicy: + operator: All + targetReplicatedJobs: [driver] + + # Any critical failure tears down everything (replaces peer-watcher). + failurePolicy: + maxRestarts: 0 + rules: + - name: head-crash + action: FailJobSet + targetReplicatedJobs: [rl-head] + - name: driver-crash + action: FailJobSet + targetReplicatedJobs: [driver] + - name: gym-crash + action: FailJobSet + targetReplicatedJobs: [gym] + - name: worker-crash + action: FailJobSet + targetReplicatedJobs: [rl-workers] + + replicatedJobs: + # ======================== + # RL Ray head (long-running daemon) + # ======================== + - name: rl-head + replicas: 1 + template: + spec: + backoffLimit: 0 + completions: 1 + parallelism: 1 + template: + spec: + schedulerName: kai-scheduler + serviceAccountName: nemo-rl-endpoint-registry + imagePullSecrets: + - name: nvcr-secret + containers: + - name: ray-head + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + command: ["/bin/bash", "-c"] + args: + - | + ulimit -n 65536 + ray start --head --port=6379 \ + --dashboard-host=0.0.0.0 \ + --object-store-memory=200000000 \ + --block + ports: + - { containerPort: 6379, name: gcs-server } + - { containerPort: 8265, name: dashboard } + - { containerPort: 10001, name: client } + readinessProbe: + exec: + command: ["ray", "health-check"] + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 5 + resources: + requests: { cpu: "1", memory: "4Gi" } + limits: { cpu: "4", memory: "16Gi" } + env: + - name: RAY_ADDRESS + value: "127.0.0.1:6379" + volumeMounts: + - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + - { name: dshm, mountPath: /dev/shm } + volumes: + - name: nemo-rl-src + hostPath: { path: /workspace/nemo-rl, type: DirectoryOrCreate } + - name: dshm + emptyDir: { medium: Memory } + + # ======================== + # RL GPU workers + # ======================== + - name: rl-workers + replicas: 1 + dependsOn: + - name: rl-head + status: Ready + template: + spec: + backoffLimit: 0 + completions: 1 + parallelism: 1 + template: + spec: + schedulerName: kai-scheduler + serviceAccountName: nemo-rl-endpoint-registry + imagePullSecrets: + - name: nvcr-secret + containers: + - name: ray-worker + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + command: ["/bin/bash", "-c"] + args: + - | + ulimit -n 65536 + HEAD=disagg-job-rl-head-0-0.disagg-job + ray start --address=${HEAD}:6379 \ + --num-gpus=2 \ + --object-store-memory=200000000 \ + --block + resources: + requests: { cpu: "2", memory: "8Gi", nvidia.com/gpu: "2" } + limits: { cpu: "8", memory: "32Gi", nvidia.com/gpu: "2" } + env: + - name: RAY_ADDRESS + value: "disagg-job-rl-head-0-0.disagg-job:6379" + volumeMounts: + - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + - { name: dshm, mountPath: /dev/shm } + volumes: + - name: nemo-rl-src + hostPath: { path: /workspace/nemo-rl, type: DirectoryOrCreate } + - name: dshm + emptyDir: { medium: Memory } + + # ======================== + # Gym standalone server (CPU only) + # ======================== + - name: gym + replicas: 1 + dependsOn: + - name: rl-head + status: Ready + template: + spec: + backoffLimit: 0 + completions: 1 + parallelism: 1 + template: + spec: + schedulerName: kai-scheduler + serviceAccountName: nemo-rl-endpoint-registry + imagePullSecrets: + - name: nvcr-secret + containers: + - name: gym-server + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + command: ["/bin/bash", "-c"] + args: + - | + cd /workspace/nemo-rl + uv run --extra nemo_gym python -m nemo_rl.distributed.standalone_gym_server \ + --job-id disagg-job \ + --port 9090 \ + --model-name Qwen/Qwen3-0.6B \ + --config-yaml infra/examples/gym_standalone_config.yaml + ports: + - { containerPort: 9090, name: head-server } + resources: + requests: { cpu: "1", memory: "4Gi" } + limits: { cpu: "4", memory: "16Gi" } + volumeMounts: + - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + volumes: + - name: nemo-rl-src + hostPath: { path: /workspace/nemo-rl, type: DirectoryOrCreate } + + # ======================== + # Driver (submits GRPO, gates success) + # ======================== + - name: driver + replicas: 1 + dependsOn: + - name: rl-head + status: Ready + template: + spec: + backoffLimit: 0 + completions: 1 + parallelism: 1 + template: + spec: + schedulerName: kai-scheduler + serviceAccountName: nemo-rl-endpoint-registry + imagePullSecrets: + - name: nvcr-secret + containers: + - name: driver + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + command: ["/bin/bash", "-c"] + args: + - | + HEAD=disagg-job-rl-head-0-0.disagg-job + # Wait for Ray dashboard to be reachable. + until curl -sf http://${HEAD}:8265/api/version >/dev/null 2>&1; do + echo "Waiting for Ray dashboard at ${HEAD}:8265..." + sleep 5 + done + echo "Ray dashboard is up, submitting job..." + # Submit GRPO training with disaggregated Gym. + ray job submit --address http://${HEAD}:8265 \ + --submission-id grpo-run \ + --working-dir /workspace/nemo-rl \ + -- python examples/nemo_gym/run_grpo_nemo_gym.py \ + +env.disagg_job_id=disagg-job + # Follow logs until completion (ray job submit --follow is not reliable). + ray job logs --follow --address http://${HEAD}:8265 grpo-run + # Exit with the job's exit code. + STATUS=$(ray job status --address http://${HEAD}:8265 grpo-run 2>/dev/null | grep -oP 'status: \K\w+') + [ "$STATUS" = "SUCCEEDED" ] && exit 0 || exit 1 + resources: + requests: { cpu: "500m", memory: "1Gi" } + limits: { cpu: "1", memory: "2Gi" } + env: + - name: RAY_ADDRESS + value: "disagg-job-rl-head-0-0.disagg-job:6379" + volumeMounts: + - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + volumes: + - name: nemo-rl-src + hostPath: { path: /workspace/nemo-rl, type: DirectoryOrCreate } diff --git a/infra/examples/rayjob-monolithic.yaml b/infra/examples/rayjob-monolithic.yaml new file mode 100644 index 00000000000..fd9a445be1d --- /dev/null +++ b/infra/examples/rayjob-monolithic.yaml @@ -0,0 +1,83 @@ +# Monolithic RayJob: single-cluster training with 1 GPU worker. +# +# KubeRay creates a RayCluster, waits for it to be ready, submits the +# entrypoint via HTTP, polls until completion, then tears down the cluster. +# +# Prerequisites: +# kubectl apply -f kai-queue.yaml +# +# Usage: +# kubectl apply -f rayjob-monolithic.yaml +# kubectl get rayjob sft-job -w + +apiVersion: ray.io/v1 +kind: RayJob +metadata: + name: sft-job + labels: + kai.scheduler/queue: high-prio +spec: + entrypoint: "bash tests/functional/sft.sh" + submissionMode: HTTPMode # K8sJobMode breaks KAI gang scheduling + shutdownAfterJobFinishes: true + ttlSecondsAfterFinished: 60 + rayClusterSpec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: + object-store-memory: "200000000" + template: + spec: + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + containers: + - name: ray-head + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + resources: + requests: { cpu: "1", memory: "4Gi" } + limits: { cpu: "4", memory: "16Gi" } + ports: + - { containerPort: 6379, name: gcs-server } + - { containerPort: 8265, name: dashboard } + readinessProbe: + exec: + command: ["ray", "health-check"] + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 5 + volumeMounts: + - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + - { name: dshm, mountPath: /dev/shm } + volumes: + - name: nemo-rl-src + hostPath: { path: /workspace/nemo-rl, type: DirectoryOrCreate } + - name: dshm + emptyDir: { medium: Memory } + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: + num-gpus: "1" + object-store-memory: "200000000" + template: + spec: + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + containers: + - name: ray-worker + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + resources: + requests: { cpu: "1", memory: "4Gi", nvidia.com/gpu: "1" } + limits: { cpu: "8", memory: "32Gi", nvidia.com/gpu: "1" } + volumeMounts: + - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + - { name: dshm, mountPath: /dev/shm } + volumes: + - name: nemo-rl-src + hostPath: { path: /workspace/nemo-rl, type: DirectoryOrCreate } + - name: dshm + emptyDir: { medium: Memory } diff --git a/infra/helm/helmfile.yaml b/infra/helm/helmfile.yaml index 8175c8833e4..502885c100c 100644 --- a/infra/helm/helmfile.yaml +++ b/infra/helm/helmfile.yaml @@ -67,3 +67,13 @@ releases: values: - values/kuberay-operator.yaml +# +# JobSet: manages groups of Jobs as a unit (alternative to KubeRay for Ray workloads) +# +- name: jobset + namespace: jobset-system + createNamespace: true + chart: oci://registry.k8s.io/jobset/charts/jobset + version: 0.11.1 + wait: true + diff --git a/infra/kind/SETUP.md b/infra/kind/SETUP.md index 9301fd82505..f80dc65c7e2 100644 --- a/infra/kind/SETUP.md +++ b/infra/kind/SETUP.md @@ -1,6 +1,6 @@ # Local K8s GPU Development Environment -nvkind-based Kubernetes cluster with NVIDIA GPU support, KAI scheduler (gang scheduling), and KubeRay. +nvkind-based Kubernetes cluster with NVIDIA GPU support, KAI scheduler (gang scheduling), KubeRay, and JobSet. ## Prerequisites @@ -67,7 +67,7 @@ Set `driver.enabled=false` in `values/gpu-operator.yaml` if the cluster nodes al | `kind` | nvidia-device-plugin | Local dev — nvkind handles toolkit/runtime | | `prod` | gpu-operator (full) | Real clusters — operator manages everything | -Both environments include KAI scheduler and KubeRay operator. +Both environments include KAI scheduler, KubeRay operator, and JobSet controller. ## Tear down (kind only) @@ -93,22 +93,37 @@ infra/ │ ├── gpu-operator.yaml # prod only │ ├── kai-scheduler.yaml │ └── kuberay-operator.yaml -├── examples/ -│ ├── disagg-rayclusters.yaml # Disaggregated RL + Gym (main exemplar) +├── examples/ # Workload examples +│ ├── rayjob-monolithic.yaml # Single-cluster RayJob (1 GPU) +│ ├── disagg-rayclusters.yaml # Disagg RL + Gym via KubeRay RayClusters +│ ├── disagg-jobset.yaml # Disagg RL + Gym via JobSet (no KubeRay) │ ├── endpoint-registry-rbac.yaml # RBAC for ConfigMap service discovery │ ├── gym_standalone_config.yaml # Gym standalone server config │ ├── kai-queue.yaml # 2-GPU kind cluster queues │ └── kai-queue-prod.yaml # 288-GPU NVL72 prod queues ``` -## Disaggregated RL + Gym +## Workload examples -The main workload exemplar is `disagg-rayclusters.yaml`: two RayClusters deployed together. +### `rayjob-monolithic.yaml` — Single-cluster RayJob +Simplest deployment: KubeRay manages a RayCluster + submits an entrypoint via HTTP. One head, one GPU worker. Good for SFT or single-node training. + +### `disagg-rayclusters.yaml` — Disagg via KubeRay + +Two RayClusters deployed together: - **RL cluster** (`raycluster-rl`): Ray head + GPU workers for training (vLLM + Megatron) -- **Gym cluster** (`raycluster-gym`): CPU-only, runs NeMo Gym servers as HTTP service -- **Service discovery**: K8s ConfigMap endpoint registry — RL publishes vLLM URLs, Gym publishes its head server address. Both poll until the peer registers. -- **Failure cascading**: Peer-watcher sidecar (inlined Python script) on each head pod. If either cluster fails or is deleted, both are torn down. +- **Gym cluster** (`raycluster-gym`): CPU-only, runs standalone Gym HTTP server +- **Service discovery**: K8s ConfigMap endpoint registry — RL publishes vLLM URLs, Gym publishes its head server address +- **Failure cascading**: Peer-watcher sidecar on each head monitors the other cluster via K8s API + +### `disagg-jobset.yaml` — Disagg via JobSet (no KubeRay) + +Same disagg architecture as above but using a single JobSet instead of two RayClusters. Four ReplicatedJobs (rl-head, rl-workers, gym, driver) with: +- **`dependsOn`**: workers, gym, and driver wait for rl-head to be Ready +- **`failurePolicy`**: any failure tears down all jobs (replaces peer-watcher) +- **`successPolicy`**: JobSet completes when driver exits 0 +- **Predictable DNS**: `disagg-job-rl-head-0-0.disagg-job` (no ConfigMap needed for head discovery) ## Notes From efb6c937814b04f55329d006fa4454d036fb8ee3 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Fri, 17 Apr 2026 23:56:41 -0700 Subject: [PATCH 14/84] infra: add gym-head + gym-workers to JobSet example Shows the full Ray cluster pattern for Gym: separate head and worker pods within the JobSet, with dependsOn ordering and DNS discovery. Signed-off-by: Terry Kong --- infra/examples/disagg-jobset.yaml | 92 ++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 13 deletions(-) diff --git a/infra/examples/disagg-jobset.yaml b/infra/examples/disagg-jobset.yaml index aa72cbd4dea..dc2cecceffc 100644 --- a/infra/examples/disagg-jobset.yaml +++ b/infra/examples/disagg-jobset.yaml @@ -1,10 +1,11 @@ # Disaggregated RL + Gym as a single JobSet (alternative to disagg-rayclusters.yaml). # -# Four ReplicatedJobs: -# rl-head — Ray head daemon (CPU), long-running -# rl-workers — GPU workers, connect to rl-head -# gym — Standalone Gym HTTP server (CPU) -# driver — Submits GRPO training, gates JobSet success +# Five ReplicatedJobs (two independent Ray clusters + a driver): +# rl-head — RL Ray head daemon (CPU), long-running +# rl-workers — RL GPU workers, connect to rl-head +# gym-head — Gym Ray head (CPU), runs standalone Gym server +# gym-workers — Gym CPU workers, connect to gym-head +# driver — Submits GRPO training, gates JobSet success # # Advantages over the KubeRay RayCluster approach: # - Native failure cascading via failurePolicy (no peer-watcher sidecar) @@ -13,10 +14,11 @@ # - Single resource to manage (one JobSet instead of two RayClusters + RBAC) # # DNS names (auto-created by JobSet headless service): -# rl-head: disagg-job-rl-head-0-0.disagg-job -# rl-workers: disagg-job-rl-workers-0-0.disagg-job (one pod per worker) -# gym: disagg-job-gym-0-0.disagg-job -# driver: disagg-job-driver-0-0.disagg-job +# rl-head: disagg-job-rl-head-0-0.disagg-job +# rl-workers: disagg-job-rl-workers-0-0.disagg-job +# gym-head: disagg-job-gym-head-0-0.disagg-job +# gym-workers: disagg-job-gym-workers-0-{0,1,...}.disagg-job +# driver: disagg-job-driver-0-0.disagg-job # # Prerequisites: # JobSet controller installed (helmfile sync installs it) @@ -61,7 +63,7 @@ spec: targetReplicatedJobs: [driver] - name: gym-crash action: FailJobSet - targetReplicatedJobs: [gym] + targetReplicatedJobs: [gym-head, gym-workers] - name: worker-crash action: FailJobSet targetReplicatedJobs: [rl-workers] @@ -166,9 +168,9 @@ spec: emptyDir: { medium: Memory } # ======================== - # Gym standalone server (CPU only) + # Gym Ray head (runs standalone Gym server on its own Ray cluster) # ======================== - - name: gym + - name: gym-head replicas: 1 dependsOn: - name: rl-head @@ -185,11 +187,17 @@ spec: imagePullSecrets: - name: nvcr-secret containers: - - name: gym-server + - name: ray-head image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 command: ["/bin/bash", "-c"] args: - | + ulimit -n 65536 + # Start Gym's own Ray head (separate cluster from RL). + ray start --head --port=6380 \ + --dashboard-host=0.0.0.0 --dashboard-port=8266 \ + --object-store-memory=200000000 + # Run the standalone Gym server on this Ray head. cd /workspace/nemo-rl uv run --extra nemo_gym python -m nemo_rl.distributed.standalone_gym_server \ --job-id disagg-job \ @@ -197,15 +205,73 @@ spec: --model-name Qwen/Qwen3-0.6B \ --config-yaml infra/examples/gym_standalone_config.yaml ports: + - { containerPort: 6380, name: gcs-server } + - { containerPort: 8266, name: dashboard } - { containerPort: 9090, name: head-server } + readinessProbe: + exec: + command: ["ray", "health-check", "--address", "127.0.0.1:6380"] + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 5 + resources: + requests: { cpu: "1", memory: "4Gi" } + limits: { cpu: "4", memory: "16Gi" } + env: + - name: RAY_ADDRESS + value: "127.0.0.1:6380" + volumeMounts: + - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + - { name: dshm, mountPath: /dev/shm } + volumes: + - name: nemo-rl-src + hostPath: { path: /workspace/nemo-rl, type: DirectoryOrCreate } + - name: dshm + emptyDir: { medium: Memory } + + # ======================== + # Gym CPU workers (connect to gym-head's Ray cluster) + # ======================== + - name: gym-workers + replicas: 1 + dependsOn: + - name: gym-head + status: Ready + template: + spec: + backoffLimit: 0 + completions: 2 + parallelism: 2 + template: + spec: + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + containers: + - name: ray-worker + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + command: ["/bin/bash", "-c"] + args: + - | + ulimit -n 65536 + GYM_HEAD=disagg-job-gym-head-0-0.disagg-job + ray start --address=${GYM_HEAD}:6380 \ + --object-store-memory=200000000 \ + --block resources: requests: { cpu: "1", memory: "4Gi" } limits: { cpu: "4", memory: "16Gi" } + env: + - name: RAY_ADDRESS + value: "disagg-job-gym-head-0-0.disagg-job:6380" volumeMounts: - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + - { name: dshm, mountPath: /dev/shm } volumes: - name: nemo-rl-src hostPath: { path: /workspace/nemo-rl, type: DirectoryOrCreate } + - name: dshm + emptyDir: { medium: Memory } # ======================== # Driver (submits GRPO, gates success) From 4043b968a63bd68b3b636d3400bf5fb59898ff04 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sat, 18 Apr 2026 00:07:45 -0700 Subject: [PATCH 15/84] =?UTF-8?q?infra:=20fix=20JobSet=20example=20?= =?UTF-8?q?=E2=80=94=20use=20init=20containers=20instead=20of=20dependsOn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KAI gang-schedules all pods in a JobSet together (one PodGroup with minMember=total pods). This deadlocks with dependsOn: KAI waits for all pods to exist, but JobSet won't create dependent pods until the head is Ready. Fix: drop dependsOn, use init containers that poll ray health-check (same pattern KubeRay uses). Tested: all 6 pods schedule, init containers wait for heads, driver submits a Ray job successfully, successPolicy triggers on driver exit 0, failurePolicy tears down everything on gym-head crash. Signed-off-by: Terry Kong --- infra/examples/disagg-jobset.yaml | 84 +++++++++++++++++++------------ 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/infra/examples/disagg-jobset.yaml b/infra/examples/disagg-jobset.yaml index dc2cecceffc..a66716f80d3 100644 --- a/infra/examples/disagg-jobset.yaml +++ b/infra/examples/disagg-jobset.yaml @@ -9,10 +9,14 @@ # # Advantages over the KubeRay RayCluster approach: # - Native failure cascading via failurePolicy (no peer-watcher sidecar) -# - Startup ordering via dependsOn (no init container polling) -# - Predictable DNS names (no Service or ConfigMap needed for discovery) +# - Predictable DNS names (no Service or ConfigMap needed for head discovery) # - Single resource to manage (one JobSet instead of two RayClusters + RBAC) # +# Note: KAI gang-schedules all pods in the JobSet together (one PodGroup), +# so dependsOn cannot be used (it would deadlock — KAI waits for all pods, +# JobSet waits for head to be Ready). Instead, workers use init containers +# to wait for their head, same pattern as KubeRay. +# # DNS names (auto-created by JobSet headless service): # rl-head: disagg-job-rl-head-0-0.disagg-job # rl-workers: disagg-job-rl-workers-0-0.disagg-job @@ -40,7 +44,6 @@ spec: enableDNSHostnames: true publishNotReadyAddresses: true - # Head pod address propagated to all pods as a label. coordinator: replicatedJob: rl-head jobIndex: 0 @@ -51,20 +54,20 @@ spec: operator: All targetReplicatedJobs: [driver] - # Any critical failure tears down everything (replaces peer-watcher). + # Any failure tears down everything (replaces peer-watcher). failurePolicy: maxRestarts: 0 rules: - - name: head-crash + - name: head_crash action: FailJobSet targetReplicatedJobs: [rl-head] - - name: driver-crash + - name: driver_crash action: FailJobSet targetReplicatedJobs: [driver] - - name: gym-crash + - name: gym_crash action: FailJobSet targetReplicatedJobs: [gym-head, gym-workers] - - name: worker-crash + - name: worker_crash action: FailJobSet targetReplicatedJobs: [rl-workers] @@ -126,9 +129,6 @@ spec: # ======================== - name: rl-workers replicas: 1 - dependsOn: - - name: rl-head - status: Ready template: spec: backoffLimit: 0 @@ -140,6 +140,20 @@ spec: serviceAccountName: nemo-rl-endpoint-registry imagePullSecrets: - name: nvcr-secret + initContainers: + - name: wait-for-rl-head + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + command: ["/bin/bash", "-c"] + args: + - | + HEAD=disagg-job-rl-head-0-0.disagg-job + echo "Waiting for RL head at ${HEAD}:6379..." + until ray health-check --address ${HEAD}:6379 >/dev/null 2>&1; do + sleep 5 + done + echo "RL head is ready." + resources: + requests: { cpu: "200m", memory: "256Mi" } containers: - name: ray-worker image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 @@ -172,9 +186,6 @@ spec: # ======================== - name: gym-head replicas: 1 - dependsOn: - - name: rl-head - status: Ready template: spec: backoffLimit: 0 @@ -193,13 +204,11 @@ spec: args: - | ulimit -n 65536 - # Start Gym's own Ray head (separate cluster from RL). ray start --head --port=6380 \ --dashboard-host=0.0.0.0 --dashboard-port=8266 \ --object-store-memory=200000000 - # Run the standalone Gym server on this Ray head. cd /workspace/nemo-rl - uv run --extra nemo_gym python -m nemo_rl.distributed.standalone_gym_server \ + python -m nemo_rl.distributed.standalone_gym_server \ --job-id disagg-job \ --port 9090 \ --model-name Qwen/Qwen3-0.6B \ @@ -234,9 +243,6 @@ spec: # ======================== - name: gym-workers replicas: 1 - dependsOn: - - name: gym-head - status: Ready template: spec: backoffLimit: 0 @@ -247,6 +253,20 @@ spec: schedulerName: kai-scheduler imagePullSecrets: - name: nvcr-secret + initContainers: + - name: wait-for-gym-head + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + command: ["/bin/bash", "-c"] + args: + - | + GYM_HEAD=disagg-job-gym-head-0-0.disagg-job + echo "Waiting for Gym head at ${GYM_HEAD}:6380..." + until ray health-check --address ${GYM_HEAD}:6380 >/dev/null 2>&1; do + sleep 5 + done + echo "Gym head is ready." + resources: + requests: { cpu: "200m", memory: "256Mi" } containers: - name: ray-worker image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 @@ -278,9 +298,6 @@ spec: # ======================== - name: driver replicas: 1 - dependsOn: - - name: rl-head - status: Ready template: spec: backoffLimit: 0 @@ -292,28 +309,33 @@ spec: serviceAccountName: nemo-rl-endpoint-registry imagePullSecrets: - name: nvcr-secret - containers: - - name: driver + initContainers: + - name: wait-for-rl-head image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 command: ["/bin/bash", "-c"] args: - | HEAD=disagg-job-rl-head-0-0.disagg-job - # Wait for Ray dashboard to be reachable. + echo "Waiting for RL Ray dashboard at ${HEAD}:8265..." until curl -sf http://${HEAD}:8265/api/version >/dev/null 2>&1; do - echo "Waiting for Ray dashboard at ${HEAD}:8265..." sleep 5 done - echo "Ray dashboard is up, submitting job..." - # Submit GRPO training with disaggregated Gym. + echo "RL Ray dashboard is up." + resources: + requests: { cpu: "200m", memory: "256Mi" } + containers: + - name: driver + image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + command: ["/bin/bash", "-c"] + args: + - | + HEAD=disagg-job-rl-head-0-0.disagg-job ray job submit --address http://${HEAD}:8265 \ --submission-id grpo-run \ --working-dir /workspace/nemo-rl \ -- python examples/nemo_gym/run_grpo_nemo_gym.py \ +env.disagg_job_id=disagg-job - # Follow logs until completion (ray job submit --follow is not reliable). ray job logs --follow --address http://${HEAD}:8265 grpo-run - # Exit with the job's exit code. STATUS=$(ray job status --address http://${HEAD}:8265 grpo-run 2>/dev/null | grep -oP 'status: \K\w+') [ "$STATUS" = "SUCCEEDED" ] && exit 0 || exit 1 resources: From 42a509c6990a7b5c588d61aadb0e570004a4a820 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sat, 18 Apr 2026 00:12:07 -0700 Subject: [PATCH 16/84] infra: use uv run --extra nemo_gym for gym-head in JobSet Signed-off-by: Terry Kong --- infra/examples/disagg-jobset.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/examples/disagg-jobset.yaml b/infra/examples/disagg-jobset.yaml index a66716f80d3..4609b5c2891 100644 --- a/infra/examples/disagg-jobset.yaml +++ b/infra/examples/disagg-jobset.yaml @@ -208,7 +208,7 @@ spec: --dashboard-host=0.0.0.0 --dashboard-port=8266 \ --object-store-memory=200000000 cd /workspace/nemo-rl - python -m nemo_rl.distributed.standalone_gym_server \ + uv run --extra nemo_gym python -m nemo_rl.distributed.standalone_gym_server \ --job-id disagg-job \ --port 9090 \ --model-name Qwen/Qwen3-0.6B \ From 145f02fd979f8a288aaf7b58ee09ae390cbb6c4c Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sat, 18 Apr 2026 00:20:24 -0700 Subject: [PATCH 17/84] infra: fix gym-head startup in JobSet example - Add git safe.directory for Gym submodule (uv build fails otherwise) - Add uv pip install kubernetes (needed by endpoint registry) - Increase readiness probe failureThreshold (uv install takes time) Signed-off-by: Terry Kong --- infra/examples/disagg-jobset.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infra/examples/disagg-jobset.yaml b/infra/examples/disagg-jobset.yaml index 4609b5c2891..eff6d997a4e 100644 --- a/infra/examples/disagg-jobset.yaml +++ b/infra/examples/disagg-jobset.yaml @@ -207,7 +207,9 @@ spec: ray start --head --port=6380 \ --dashboard-host=0.0.0.0 --dashboard-port=8266 \ --object-store-memory=200000000 + git config --global --add safe.directory /workspace/nemo-rl/3rdparty/Gym-workspace/Gym cd /workspace/nemo-rl + uv pip install kubernetes uv run --extra nemo_gym python -m nemo_rl.distributed.standalone_gym_server \ --job-id disagg-job \ --port 9090 \ @@ -223,6 +225,7 @@ spec: initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 5 + failureThreshold: 60 resources: requests: { cpu: "1", memory: "4Gi" } limits: { cpu: "4", memory: "16Gi" } From 7b2e38f45f52b6714a39ea48aa473ee81e8bf939 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sat, 18 Apr 2026 00:37:28 -0700 Subject: [PATCH 18/84] infra: fix driver working-dir upload in JobSet example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace --working-dir and --runtime-env-json with a simple cd wrapper. Both --working-dir and runtime_env.working_dir cause Ray to zip and upload the entire directory to GCS, which is extremely slow for large repos (1GB+). Since the code is already on all nodes via hostPath, wrapping the entrypoint with cd avoids the upload entirely. Before: ray job submit --working-dir /workspace/nemo-rl -- python ... → scans entire tree, uploads 1GB+ to GCS, takes minutes After: ray job submit -- bash -c "cd /workspace/nemo-rl && python ..." → instant submission, no upload Signed-off-by: Terry Kong --- infra/examples/disagg-jobset.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/infra/examples/disagg-jobset.yaml b/infra/examples/disagg-jobset.yaml index eff6d997a4e..585d2536d50 100644 --- a/infra/examples/disagg-jobset.yaml +++ b/infra/examples/disagg-jobset.yaml @@ -333,11 +333,12 @@ spec: args: - | HEAD=disagg-job-rl-head-0-0.disagg-job + # No --working-dir: code is on all nodes via hostPath. + # --working-dir triggers Ray to zip+upload the directory to GCS. + # Instead, wrap the entrypoint with cd. ray job submit --address http://${HEAD}:8265 \ --submission-id grpo-run \ - --working-dir /workspace/nemo-rl \ - -- python examples/nemo_gym/run_grpo_nemo_gym.py \ - +env.disagg_job_id=disagg-job + -- bash -c "cd /workspace/nemo-rl && python examples/nemo_gym/run_grpo_nemo_gym.py +env.disagg_job_id=disagg-job" ray job logs --follow --address http://${HEAD}:8265 grpo-run STATUS=$(ray job status --address http://${HEAD}:8265 grpo-run 2>/dev/null | grep -oP 'status: \K\w+') [ "$STATUS" = "SUCCEEDED" ] && exit 0 || exit 1 From dde17fe595161011908189b2fcaeb373ac643059 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sat, 18 Apr 2026 01:11:29 -0700 Subject: [PATCH 19/84] =?UTF-8?q?infra:=20fix=20driver=20in=20JobSet=20?= =?UTF-8?q?=E2=80=94=20unique=20submission=5Fid,=20disable=20wandb,=20pipe?= =?UTF-8?q?fail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use timestamp-based submission_id to avoid GCS collision across redeploys - Disable wandb (not configured in kind dev cluster) - Add set -eo pipefail for proper exit code propagation through tee - Persist driver logs to hostPath for post-mortem debugging Tested: GRPO training loads config, connects to Ray cluster, loads datasets, initializes compute cluster. Fails with "Not enough GPUs" (expected — kind cluster has 2 GPUs, config expects 8). Signed-off-by: Terry Kong --- infra/examples/disagg-jobset.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/infra/examples/disagg-jobset.yaml b/infra/examples/disagg-jobset.yaml index 585d2536d50..4b01614a755 100644 --- a/infra/examples/disagg-jobset.yaml +++ b/infra/examples/disagg-jobset.yaml @@ -332,16 +332,16 @@ spec: command: ["/bin/bash", "-c"] args: - | + set -eo pipefail HEAD=disagg-job-rl-head-0-0.disagg-job # No --working-dir: code is on all nodes via hostPath. # --working-dir triggers Ray to zip+upload the directory to GCS. # Instead, wrap the entrypoint with cd. + SUBMISSION_ID="grpo-$(date +%s)" ray job submit --address http://${HEAD}:8265 \ - --submission-id grpo-run \ - -- bash -c "cd /workspace/nemo-rl && python examples/nemo_gym/run_grpo_nemo_gym.py +env.disagg_job_id=disagg-job" - ray job logs --follow --address http://${HEAD}:8265 grpo-run - STATUS=$(ray job status --address http://${HEAD}:8265 grpo-run 2>/dev/null | grep -oP 'status: \K\w+') - [ "$STATUS" = "SUCCEEDED" ] && exit 0 || exit 1 + --submission-id ${SUBMISSION_ID} \ + -- bash -c "cd /workspace/nemo-rl && python examples/nemo_gym/run_grpo_nemo_gym.py +env.disagg_job_id=disagg-job logger.wandb_enabled=false" \ + 2>&1 | tee /workspace/nemo-rl/outputs/driver.log resources: requests: { cpu: "500m", memory: "1Gi" } limits: { cpu: "1", memory: "2Gi" } From 05cd1d299faead5a716315f10ea47608e5182175 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sat, 18 Apr 2026 01:18:31 -0700 Subject: [PATCH 20/84] infra: rewrite SETUP.md with architecture docs, disclaimers, and guides - Add active development disclaimer (GitHub admonition) - Add production guidance (adapt manifests, use Terraform, not helmfile) - Document colocated vs disaggregated architecture with diagrams - Compare KubeRay RayClusters vs JobSet for disagg deployment - Explain why ConfigMap is still needed for JobSet (vLLM URL exchange) - Document dependsOn + KAI deadlock and init container workaround - Add local kind testing instructions - Add comparison table (failure cascading, gang scheduling, discovery) Signed-off-by: Terry Kong --- infra/kind/SETUP.md | 149 +++++++++++++++++++++++++++++++++----------- 1 file changed, 114 insertions(+), 35 deletions(-) diff --git a/infra/kind/SETUP.md b/infra/kind/SETUP.md index f80dc65c7e2..8a472d2dbc8 100644 --- a/infra/kind/SETUP.md +++ b/infra/kind/SETUP.md @@ -1,6 +1,14 @@ -# Local K8s GPU Development Environment +# K8s Infrastructure for nemo-rl -nvkind-based Kubernetes cluster with NVIDIA GPU support, KAI scheduler (gang scheduling), KubeRay, and JobSet. +> [!WARNING] +> These instructions are in active development and should not be relied on as stable yet. +> APIs, manifests, and tooling may change without notice. + +## Overview + +This gives you a local GPU-enabled K8s playground for testing RL workloads — all you need is Docker and an NVIDIA GPU. The same example manifests can be brought to a production K8s cluster, but you may need to adapt them (work with your cluster operator). + +The helmfile here is for convenience. A production system should manage these components in Terraform or another infrastructure-as-code solution. ## Prerequisites @@ -35,7 +43,7 @@ bash get-helm.sh # 2. Create cluster (all host GPUs exposed to a single worker node) bash create-cluster.sh -# 3. Deploy infrastructure +# 3. Deploy infrastructure (KAI scheduler, KubeRay, JobSet controller) cd ../helm helmfile -e kind sync @@ -43,9 +51,26 @@ helmfile -e kind sync kubectl apply -f ../examples/kai-queue.yaml kubectl apply -f ../examples/endpoint-registry-rbac.yaml -# 5. Deploy disaggregated RL + Gym -kubectl apply -f ../examples/disagg-rayclusters.yaml -kubectl get rayclusters -w # both should become "ready" +# 5. Deploy a workload (pick one) +kubectl apply -f ../examples/rayjob-monolithic.yaml # single-cluster RayJob +kubectl apply -f ../examples/disagg-rayclusters.yaml # disagg via KubeRay +kubectl apply -f ../examples/disagg-jobset.yaml # disagg via JobSet +``` + +### Testing locally with kind + +Once the cluster is up, you can: + +```sh +kubectl get rayclusters -w # watch cluster status +kubectl get jobsets.jobset.x-k8s.io # watch JobSet status +kubectl get pods -o wide # see pod placement and IPs +kubectl logs # check logs + +# Exec into RL head to run training manually: +kubectl exec -it -c ray-head -- bash +cd /workspace/nemo-rl +python examples/nemo_gym/run_grpo_nemo_gym.py +env.disagg_job_id=my-job logger.wandb_enabled=false ``` ## Deploy on a real cluster @@ -56,26 +81,87 @@ helmfile -e prod sync kubectl apply -f infra/examples/kai-queue-prod.yaml ``` -This installs the full **GPU Operator** (instead of just the device plugin) along with KAI scheduler and KubeRay. The GPU Operator manages the NVIDIA driver, container toolkit, device plugin, NFD, and DCGM exporter. +This installs the full **GPU Operator** (instead of just the device plugin) along with KAI scheduler, KubeRay, and JobSet. The GPU Operator manages the NVIDIA driver, container toolkit, device plugin, NFD, and DCGM exporter. Set `driver.enabled=false` in `values/gpu-operator.yaml` if the cluster nodes already have the NVIDIA driver installed. -## Helmfile environments +## Architecture -| Environment | GPU component | Use case | -|-------------|---------------|----------| -| `kind` | nvidia-device-plugin | Local dev — nvkind handles toolkit/runtime | -| `prod` | gpu-operator (full) | Real clusters — operator manages everything | +### Colocated (single Ray cluster) -Both environments include KAI scheduler, KubeRay operator, and JobSet controller. +All components run on a single RayCluster — vLLM generation, Megatron training, and Gym environment servers are colocated as Ray actors on the same cluster. -## Tear down (kind only) +``` +┌─────────────────────────────────────────────┐ +│ RayCluster / RayJob │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ vLLM │ │ Megatron │ │ Gym │ │ +│ │ (GPU) │ │ (GPU) │ │ (CPU) │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ All on same Ray cluster │ +└─────────────────────────────────────────────┘ +``` + +Example: `rayjob-monolithic.yaml` — a single RayJob with head + GPU workers. KubeRay manages the lifecycle. + +### Disaggregated (separate RL + Gym clusters) + +The RL cluster (vLLM + Megatron) and Gym cluster (environment servers) run independently and communicate over HTTP. A K8s ConfigMap acts as an endpoint registry for dynamic URL exchange — RL publishes vLLM URLs, Gym publishes its head server address. -```sh -kind delete cluster --name nemo-rl +``` +┌──────────────────────────┐ ┌──────────────────────────┐ +│ RL Ray Cluster │ │ Gym Ray Cluster │ +│ │ │ │ +│ ┌──────┐ ┌──────────┐ │ │ ┌──────────┐ │ +│ │ vLLM │ │ Megatron │ │ │ │ Gym │ │ +│ │ (GPU)│ │ (GPU) │ │ │ │ servers │ │ +│ └──┬───┘ └──────────┘ │ │ └────┬─────┘ │ +│ │ │ │ │ │ +└─────┼────────────────────┘ └───────┼───────────────────┘ + │ │ + │ ┌──────────────────┐ │ + └────►│ ConfigMap │◄────────┘ + │ (endpoint │ + │ registry) │ + └──────────────────┘ + vLLM URLs ←→ Gym address ``` -## Architecture +There are two ways to deploy the disagg architecture: + +#### Option A: Two KubeRay RayClusters (`disagg-rayclusters.yaml`) + +- KubeRay operator manages each RayCluster independently +- **Failure cascading** requires a peer-watcher sidecar on each head pod (inlined Python script that monitors the peer cluster via K8s API and tears down both on failure) +- **Startup ordering** is implicit — both clusters start simultaneously, the endpoint registry handles coordination +- **Gang-of-gang scheduling** is not natively supported by KAI — each RayCluster gets its own PodGroup, and KAI cannot gang two PodGroups together. See [KAI issue #1420](https://github.com/kai-scheduler/KAI-Scheduler/issues/1420). The peer-watcher is a workaround +- **ConfigMap** is required for both vLLM URL exchange and Gym head address discovery + +#### Option B: Single JobSet (`disagg-jobset.yaml`) + +- JobSet controller manages all pods as a single unit (no KubeRay needed for lifecycle) +- **Failure cascading** is native via `failurePolicy: FailJobSet` — any job failure tears down everything +- **Startup ordering** uses init containers (not `dependsOn` — see note below). Workers wait for their head via `ray health-check` polling, same pattern as KubeRay +- **Gang scheduling** works naturally — KAI creates one PodGroup for the entire JobSet, so all pods are gang-scheduled together +- **DNS names** are predictable (`disagg-job-rl-head-0-0.disagg-job`), so the Gym head address can be hardcoded instead of discovered via ConfigMap. However, **ConfigMap is still needed for vLLM URL exchange** — vLLM binds to a dynamic IP:port inside the RL worker, which isn't known until runtime + +> [!NOTE] +> **Why not `dependsOn`?** KAI gang-schedules all pods in a JobSet together (one PodGroup with `minMember` = total pods). `dependsOn` prevents dependent pods from being created until the head is Ready. This deadlocks: KAI waits for all pods to exist, JobSet waits for the head to be scheduled. The fix is to create all pods simultaneously and use init containers for ordering. + +### Comparison + +| Feature | Colocated | Disagg (KubeRay) | Disagg (JobSet) | +|---------|-----------|-------------------|-----------------| +| Resources to manage | 1 RayJob | 2 RayClusters + RBAC | 1 JobSet + RBAC | +| Failure cascading | KubeRay built-in | Peer-watcher sidecar | Native failurePolicy | +| Gang scheduling | Single PodGroup | Two PodGroups (no cross-gang) | Single PodGroup | +| Head discovery | KubeRay Service | ConfigMap registry | DNS (predictable) | +| vLLM URL exchange | N/A (colocated) | ConfigMap registry | ConfigMap registry | +| Startup ordering | KubeRay built-in | Implicit (both start) | Init containers | +| KubeRay required | Yes | Yes | No | + +## File layout ``` infra/ @@ -103,27 +189,20 @@ infra/ │ └── kai-queue-prod.yaml # 288-GPU NVL72 prod queues ``` -## Workload examples - -### `rayjob-monolithic.yaml` — Single-cluster RayJob - -Simplest deployment: KubeRay manages a RayCluster + submits an entrypoint via HTTP. One head, one GPU worker. Good for SFT or single-node training. +## Helmfile environments -### `disagg-rayclusters.yaml` — Disagg via KubeRay +| Environment | GPU component | Use case | +|-------------|---------------|----------| +| `kind` | nvidia-device-plugin | Local dev — nvkind handles toolkit/runtime | +| `prod` | gpu-operator (full) | Real clusters — operator manages everything | -Two RayClusters deployed together: -- **RL cluster** (`raycluster-rl`): Ray head + GPU workers for training (vLLM + Megatron) -- **Gym cluster** (`raycluster-gym`): CPU-only, runs standalone Gym HTTP server -- **Service discovery**: K8s ConfigMap endpoint registry — RL publishes vLLM URLs, Gym publishes its head server address -- **Failure cascading**: Peer-watcher sidecar on each head monitors the other cluster via K8s API +Both environments include KAI scheduler, KubeRay operator, and JobSet controller. -### `disagg-jobset.yaml` — Disagg via JobSet (no KubeRay) +## Tear down (kind only) -Same disagg architecture as above but using a single JobSet instead of two RayClusters. Four ReplicatedJobs (rl-head, rl-workers, gym, driver) with: -- **`dependsOn`**: workers, gym, and driver wait for rl-head to be Ready -- **`failurePolicy`**: any failure tears down all jobs (replaces peer-watcher) -- **`successPolicy`**: JobSet completes when driver exits 0 -- **Predictable DNS**: `disagg-job-rl-head-0-0.disagg-job` (no ConfigMap needed for head discovery) +```sh +kind delete cluster --name nemo-rl +``` ## Notes @@ -157,7 +236,7 @@ KAI distributes GPU resources using hierarchical fair-share with two phases: ### Example configs -- `kai-queue.yaml` — 2-GPU kind cluster (priority-team + community, imbalanced) +- `kai-queue.yaml` — 2-GPU kind cluster (high-prio + low-prio) - `kai-queue-prod.yaml` — 288-GPU NVL72 production cluster (priority + community departments) ## TODO: NVL72 topology-aware scheduling From a545dd0e709f98765e5e05d953ebcc8a2bd6a26d Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sat, 18 Apr 2026 11:01:13 -0700 Subject: [PATCH 21/84] infra: fix GRPO config for 2-GPU kind cluster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add all parallelism overrides needed for Qwen3-0.6B on a 2-GPU cluster: - tensor_model_parallel_size=1, pipeline=1, expert=1, context=1 - sequence_parallel=false (requires TP>1) - colocated.enabled=false, gpus_per_node=1 - max_new_tokens=512, max_total_sequence_length=512 - max_num_steps=2 for quick smoke testing Tested: GRPO initializes vLLM workers, captures CUDA graphs, starts Megatron LM workers. Fails at k8s_endpoint_registry import because the container image predates the tk/infra branch — the hostPath mount has newer code than the baked-in worker venvs. Will work with a matching container build. Signed-off-by: Terry Kong --- infra/examples/disagg-jobset.yaml | 34 +++++++++++++++++++------- infra/examples/disagg-rayclusters.yaml | 25 ++++++++++++++++++- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/infra/examples/disagg-jobset.yaml b/infra/examples/disagg-jobset.yaml index 4b01614a755..562f04ae462 100644 --- a/infra/examples/disagg-jobset.yaml +++ b/infra/examples/disagg-jobset.yaml @@ -319,11 +319,11 @@ spec: args: - | HEAD=disagg-job-rl-head-0-0.disagg-job - echo "Waiting for RL Ray dashboard at ${HEAD}:8265..." - until curl -sf http://${HEAD}:8265/api/version >/dev/null 2>&1; do + echo "Waiting for RL head at ${HEAD}:6379..." + until ray health-check --address ${HEAD}:6379 >/dev/null 2>&1; do sleep 5 done - echo "RL Ray dashboard is up." + echo "RL head is ready." resources: requests: { cpu: "200m", memory: "256Mi" } containers: @@ -334,17 +334,33 @@ spec: - | set -eo pipefail HEAD=disagg-job-rl-head-0-0.disagg-job - # No --working-dir: code is on all nodes via hostPath. - # --working-dir triggers Ray to zip+upload the directory to GCS. - # Instead, wrap the entrypoint with cd. SUBMISSION_ID="grpo-$(date +%s)" ray job submit --address http://${HEAD}:8265 \ --submission-id ${SUBMISSION_ID} \ - -- bash -c "cd /workspace/nemo-rl && python examples/nemo_gym/run_grpo_nemo_gym.py +env.disagg_job_id=disagg-job logger.wandb_enabled=false" \ + -- bash -c "cd /workspace/nemo-rl && python examples/nemo_gym/run_grpo_nemo_gym.py \ + --config examples/nemo_gym/grpo_qwen3_30ba3b_instruct.yaml \ + +env.disagg_job_id=disagg-job \ + policy.model_name=Qwen/Qwen3-0.6B \ + cluster.gpus_per_node=2 \ + policy.megatron_cfg.tensor_model_parallel_size=1 \ + policy.megatron_cfg.pipeline_model_parallel_size=1 \ + policy.megatron_cfg.expert_model_parallel_size=1 \ + policy.megatron_cfg.context_parallel_size=1 \ + policy.megatron_cfg.sequence_parallel=false \ + policy.generation.vllm_cfg.tensor_parallel_size=1 \ + policy.generation.colocated.enabled=false \ + policy.generation.colocated.resources.num_nodes=1 \ + policy.generation.colocated.resources.gpus_per_node=1 \ + policy.generation.max_new_tokens=512 \ + policy.max_total_sequence_length=512 \ + grpo.num_prompts_per_step=4 grpo.num_generations_per_prompt=2 \ + grpo.max_num_steps=2 \ + policy.train_global_batch_size=4 policy.train_micro_batch_size=1 \ + logger.wandb_enabled=false" \ 2>&1 | tee /workspace/nemo-rl/outputs/driver.log resources: - requests: { cpu: "500m", memory: "1Gi" } - limits: { cpu: "1", memory: "2Gi" } + requests: { cpu: "500m", memory: "2Gi" } + limits: { cpu: "2", memory: "8Gi" } env: - name: RAY_ADDRESS value: "disagg-job-rl-head-0-0.disagg-job:6379" diff --git a/infra/examples/disagg-rayclusters.yaml b/infra/examples/disagg-rayclusters.yaml index 68cd7c9d13d..babcc75a475 100644 --- a/infra/examples/disagg-rayclusters.yaml +++ b/infra/examples/disagg-rayclusters.yaml @@ -15,7 +15,30 @@ # # Usage: # kubectl apply -f disagg-rayclusters.yaml -# # On RL head: run GRPO with +env.disagg_job_id= +# +# # On RL head (exec into pod, submit via Ray Jobs API): +# ray job submit --address http://127.0.0.1:8265 -- bash -c \ +# "cd /workspace/nemo-rl && python examples/nemo_gym/run_grpo_nemo_gym.py \ +# --config examples/nemo_gym/grpo_qwen3_30ba3b_instruct.yaml \ +# +env.disagg_job_id= \ +# policy.model_name=Qwen/Qwen3-0.6B \ +# cluster.gpus_per_node=2 \ +# policy.megatron_cfg.tensor_model_parallel_size=1 \ +# policy.megatron_cfg.pipeline_model_parallel_size=1 \ +# policy.megatron_cfg.expert_model_parallel_size=1 \ +# policy.megatron_cfg.context_parallel_size=1 \ +# policy.megatron_cfg.sequence_parallel=false \ +# policy.generation.vllm_cfg.tensor_parallel_size=1 \ +# policy.generation.colocated.enabled=false \ +# policy.generation.colocated.resources.num_nodes=1 \ +# policy.generation.colocated.resources.gpus_per_node=1 \ +# policy.generation.max_new_tokens=512 \ +# policy.max_total_sequence_length=512 \ +# grpo.num_prompts_per_step=4 grpo.num_generations_per_prompt=2 \ +# grpo.max_num_steps=2 \ +# policy.train_global_batch_size=4 policy.train_micro_batch_size=1 \ +# logger.wandb_enabled=false" +# # # On Gym head: uv run --extra nemo_gym python -m nemo_rl.distributed.standalone_gym_server --job-id= # ======================== From c91857ccb37ceacf702fe069247015f66c34861c Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 19 Apr 2026 10:08:48 -0700 Subject: [PATCH 22/84] infra: add NRL_FORCE_REBUILD_VENVS=true to all pod specs Signed-off-by: Terry Kong --- infra/examples/disagg-jobset.yaml | 4 ++++ infra/examples/disagg-rayclusters.yaml | 5 +++++ infra/examples/rayjob-monolithic.yaml | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/infra/examples/disagg-jobset.yaml b/infra/examples/disagg-jobset.yaml index 562f04ae462..241327b22aa 100644 --- a/infra/examples/disagg-jobset.yaml +++ b/infra/examples/disagg-jobset.yaml @@ -115,6 +115,8 @@ spec: env: - name: RAY_ADDRESS value: "127.0.0.1:6379" + - name: NRL_FORCE_REBUILD_VENVS + value: "true" volumeMounts: - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } - { name: dshm, mountPath: /dev/shm } @@ -172,6 +174,8 @@ spec: env: - name: RAY_ADDRESS value: "disagg-job-rl-head-0-0.disagg-job:6379" + - name: NRL_FORCE_REBUILD_VENVS + value: "true" volumeMounts: - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } - { name: dshm, mountPath: /dev/shm } diff --git a/infra/examples/disagg-rayclusters.yaml b/infra/examples/disagg-rayclusters.yaml index babcc75a475..d8cb5b73833 100644 --- a/infra/examples/disagg-rayclusters.yaml +++ b/infra/examples/disagg-rayclusters.yaml @@ -68,6 +68,8 @@ spec: env: - name: RAY_CLUSTER_NAME value: raycluster-rl # used by endpoint registry for ownerReference + - name: NRL_FORCE_REBUILD_VENVS + value: "true" resources: limits: { cpu: "4", memory: "16Gi" } requests: { cpu: "1", memory: "4Gi" } @@ -171,6 +173,9 @@ spec: containers: - name: ray-worker image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + env: + - name: NRL_FORCE_REBUILD_VENVS + value: "true" resources: limits: { cpu: "8", memory: "32Gi", nvidia.com/gpu: "2" } requests: { cpu: "2", memory: "8Gi", nvidia.com/gpu: "2" } diff --git a/infra/examples/rayjob-monolithic.yaml b/infra/examples/rayjob-monolithic.yaml index fd9a445be1d..e54fcc71da3 100644 --- a/infra/examples/rayjob-monolithic.yaml +++ b/infra/examples/rayjob-monolithic.yaml @@ -34,6 +34,9 @@ spec: containers: - name: ray-head image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + env: + - name: NRL_FORCE_REBUILD_VENVS + value: "true" resources: requests: { cpu: "1", memory: "4Gi" } limits: { cpu: "4", memory: "16Gi" } @@ -70,6 +73,9 @@ spec: containers: - name: ray-worker image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + env: + - name: NRL_FORCE_REBUILD_VENVS + value: "true" resources: requests: { cpu: "1", memory: "4Gi", nvidia.com/gpu: "1" } limits: { cpu: "8", memory: "32Gi", nvidia.com/gpu: "1" } From 8e210807eddac4536706b25fa158b806e9c05cf6 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 19 Apr 2026 10:42:39 -0700 Subject: [PATCH 23/84] infra: mount hostPath at /opt/nemo-rl for code consistency Mount the local nemo-rl source at both /workspace/nemo-rl and /opt/nemo-rl. The container's editable install points to /opt/nemo-rl, so this ensures all imports (Ray job driver, worker venvs) use the same code from the hostPath mount. Tested: GRPO setup completes (471s), vLLM workers initialized with CUDA graphs, Megatron workers started, endpoint registry created and vLLM URLs published. Full disagg E2E validated up to Gym handshake. Signed-off-by: Terry Kong --- infra/examples/disagg-jobset.yaml | 5 +++++ infra/examples/disagg-rayclusters.yaml | 2 ++ infra/examples/rayjob-monolithic.yaml | 2 ++ 3 files changed, 9 insertions(+) diff --git a/infra/examples/disagg-jobset.yaml b/infra/examples/disagg-jobset.yaml index 241327b22aa..372934f9392 100644 --- a/infra/examples/disagg-jobset.yaml +++ b/infra/examples/disagg-jobset.yaml @@ -119,6 +119,7 @@ spec: value: "true" volumeMounts: - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + - { name: nemo-rl-src, mountPath: /opt/nemo-rl } - { name: dshm, mountPath: /dev/shm } volumes: - name: nemo-rl-src @@ -178,6 +179,7 @@ spec: value: "true" volumeMounts: - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + - { name: nemo-rl-src, mountPath: /opt/nemo-rl } - { name: dshm, mountPath: /dev/shm } volumes: - name: nemo-rl-src @@ -238,6 +240,7 @@ spec: value: "127.0.0.1:6380" volumeMounts: - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + - { name: nemo-rl-src, mountPath: /opt/nemo-rl } - { name: dshm, mountPath: /dev/shm } volumes: - name: nemo-rl-src @@ -293,6 +296,7 @@ spec: value: "disagg-job-gym-head-0-0.disagg-job:6380" volumeMounts: - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + - { name: nemo-rl-src, mountPath: /opt/nemo-rl } - { name: dshm, mountPath: /dev/shm } volumes: - name: nemo-rl-src @@ -370,6 +374,7 @@ spec: value: "disagg-job-rl-head-0-0.disagg-job:6379" volumeMounts: - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + - { name: nemo-rl-src, mountPath: /opt/nemo-rl } volumes: - name: nemo-rl-src hostPath: { path: /workspace/nemo-rl, type: DirectoryOrCreate } diff --git a/infra/examples/disagg-rayclusters.yaml b/infra/examples/disagg-rayclusters.yaml index d8cb5b73833..36c5dc457ea 100644 --- a/infra/examples/disagg-rayclusters.yaml +++ b/infra/examples/disagg-rayclusters.yaml @@ -79,6 +79,7 @@ spec: - { containerPort: 10001, name: client } volumeMounts: - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + - { name: nemo-rl-src, mountPath: /opt/nemo-rl } # --- Peer-watcher sidecar: tears down both clusters if Gym fails --- # Monitors raycluster-gym via K8s API. If peer is deleted, failed, or # signals an error via ConfigMap, deletes both RayClusters. @@ -181,6 +182,7 @@ spec: requests: { cpu: "2", memory: "8Gi", nvidia.com/gpu: "2" } volumeMounts: - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + - { name: nemo-rl-src, mountPath: /opt/nemo-rl } volumes: - name: nemo-rl-src hostPath: { path: /workspace/nemo-rl, type: DirectoryOrCreate } diff --git a/infra/examples/rayjob-monolithic.yaml b/infra/examples/rayjob-monolithic.yaml index e54fcc71da3..794c163fc25 100644 --- a/infra/examples/rayjob-monolithic.yaml +++ b/infra/examples/rayjob-monolithic.yaml @@ -51,6 +51,7 @@ spec: timeoutSeconds: 5 volumeMounts: - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + - { name: nemo-rl-src, mountPath: /opt/nemo-rl } - { name: dshm, mountPath: /dev/shm } volumes: - name: nemo-rl-src @@ -81,6 +82,7 @@ spec: limits: { cpu: "8", memory: "32Gi", nvidia.com/gpu: "1" } volumeMounts: - { name: nemo-rl-src, mountPath: /workspace/nemo-rl } + - { name: nemo-rl-src, mountPath: /opt/nemo-rl } - { name: dshm, mountPath: /dev/shm } volumes: - name: nemo-rl-src From ca474b804b98958fc67f5cffe321af2047f7fda7 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 19:30:52 -0700 Subject: [PATCH 24/84] address PR #2238 review feedback (infra part) Signed-off-by: Terry Kong --- infra/examples/disagg-rayclusters.yaml | 16 ++++++++-------- infra/examples/endpoint-registry-rbac.yaml | 4 ++++ infra/kind/nvkind-config-values-dev.yaml | 1 + 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/infra/examples/disagg-rayclusters.yaml b/infra/examples/disagg-rayclusters.yaml index 36c5dc457ea..74906620bfd 100644 --- a/infra/examples/disagg-rayclusters.yaml +++ b/infra/examples/disagg-rayclusters.yaml @@ -130,6 +130,10 @@ spec: code = r.get("code", 0) if code == 404: teardown(f"Peer {PEER} deleted") + if JOB_ID: + cm = kube(f"/api/v1/namespaces/{NS}/configmaps/nemo-rl-endpoints-{JOB_ID}") + if cm.get("data", {}).get("error", ""): + teardown(f"Error via ConfigMap: {cm['data']['error']}") state = r.get("status", {}).get("state", "") if state in ("failed", "suspended") or (code != 0 and state == ""): fails += 1 @@ -137,10 +141,6 @@ spec: if fails >= MAX_FAIL: teardown(f"Peer {PEER} failed {MAX_FAIL}x") continue - if JOB_ID: - cm = kube(f"/api/v1/namespaces/{NS}/configmaps/nemo-rl-endpoints-{JOB_ID}") - if cm.get("data", {}).get("error", ""): - teardown(f"Error via ConfigMap: {cm['data']['error']}") if fails > 0: print(f"[peer-watcher] Peer {PEER} recovered", flush=True) fails = 0 @@ -267,6 +267,10 @@ spec: code = r.get("code", 0) if code == 404: teardown(f"Peer {PEER} deleted") + if JOB_ID: + cm = kube(f"/api/v1/namespaces/{NS}/configmaps/nemo-rl-endpoints-{JOB_ID}") + if cm.get("data", {}).get("error", ""): + teardown(f"Error via ConfigMap: {cm['data']['error']}") state = r.get("status", {}).get("state", "") if state in ("failed", "suspended") or (code != 0 and state == ""): fails += 1 @@ -274,10 +278,6 @@ spec: if fails >= MAX_FAIL: teardown(f"Peer {PEER} failed {MAX_FAIL}x") continue - if JOB_ID: - cm = kube(f"/api/v1/namespaces/{NS}/configmaps/nemo-rl-endpoints-{JOB_ID}") - if cm.get("data", {}).get("error", ""): - teardown(f"Error via ConfigMap: {cm['data']['error']}") if fails > 0: print(f"[peer-watcher] Peer {PEER} recovered", flush=True) fails = 0 diff --git a/infra/examples/endpoint-registry-rbac.yaml b/infra/examples/endpoint-registry-rbac.yaml index 5e46c03b659..63496ba7c49 100644 --- a/infra/examples/endpoint-registry-rbac.yaml +++ b/infra/examples/endpoint-registry-rbac.yaml @@ -1,6 +1,10 @@ # RBAC for disaggregated RL-Gym service discovery. # Allows Ray pods to CRUD ConfigMaps (as an endpoint registry) # and read RayClusters (for ownerReference UID lookup). +# +# NOTE: This is an example for development/testing. For production deployments, +# consider scoping the Role to specific ConfigMap names (resourceNames) and +# using separate ServiceAccounts for the RL and Gym clusters. apiVersion: v1 kind: ServiceAccount metadata: diff --git a/infra/kind/nvkind-config-values-dev.yaml b/infra/kind/nvkind-config-values-dev.yaml index 368264ea7d0..20859cc572a 100644 --- a/infra/kind/nvkind-config-values-dev.yaml +++ b/infra/kind/nvkind-config-values-dev.yaml @@ -3,5 +3,6 @@ workers: - devices: all extraMounts: +# TODO: Edit hostPath to point to your local nemo-rl checkout. - hostPath: /home/terryk/nemo-rl containerPath: /workspace/nemo-rl From 53296e5e47820fc3afdf5d79ea21d77c3185d240 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 19:30:52 -0700 Subject: [PATCH 25/84] feat: nrl-k8s CLI for launching NeMo-RL recipes on Kubernetes (infra part) Signed-off-by: Terry Kong --- tools/nrl_k8s/README.md | 426 +++++++++ tools/nrl_k8s/docs/production-readiness.md | 134 +++ tools/nrl_k8s/docs/recipes.md | 419 +++++++++ tools/nrl_k8s/docs/roadmap.md | 111 +++ .../qwen3_4b_if_full_disagg.infra.yaml | 383 ++++++++ .../examples/qwen3_4b_if_full_disagg.yaml | 33 + .../qwen3_4b_if_gym_disagg.infra.yaml | 219 +++++ .../examples/qwen3_4b_if_gym_disagg.yaml | 49 ++ .../examples/qwen3_4b_if_single.infra.yaml | 166 ++++ .../nrl_k8s/examples/qwen3_4b_if_single.yaml | 50 ++ tools/nrl_k8s/pyproject.toml | 41 + tools/nrl_k8s/src/nrl_k8s/__init__.py | 3 + tools/nrl_k8s/src/nrl_k8s/_logging.py | 64 ++ tools/nrl_k8s/src/nrl_k8s/_retry.py | 78 ++ tools/nrl_k8s/src/nrl_k8s/cli.py | 818 ++++++++++++++++++ tools/nrl_k8s/src/nrl_k8s/config.py | 296 +++++++ .../nrl_k8s/defaults/defaults.example.yaml | 73 ++ tools/nrl_k8s/src/nrl_k8s/inspect.py | 179 ++++ tools/nrl_k8s/src/nrl_k8s/k8s.py | 196 +++++ tools/nrl_k8s/src/nrl_k8s/manifest.py | 104 +++ tools/nrl_k8s/src/nrl_k8s/orchestrate.py | 336 +++++++ tools/nrl_k8s/src/nrl_k8s/schema.py | 397 +++++++++ tools/nrl_k8s/src/nrl_k8s/submit.py | 326 +++++++ tools/nrl_k8s/src/nrl_k8s/workdir.py | 96 ++ tools/nrl_k8s/tests/__init__.py | 0 tools/nrl_k8s/tests/unit/__init__.py | 0 tools/nrl_k8s/tests/unit/test_cli.py | 176 ++++ tools/nrl_k8s/tests/unit/test_config.py | 217 +++++ tools/nrl_k8s/tests/unit/test_inspect.py | 142 +++ tools/nrl_k8s/tests/unit/test_k8s.py | 192 ++++ tools/nrl_k8s/tests/unit/test_manifest.py | 201 +++++ tools/nrl_k8s/tests/unit/test_orchestrate.py | 307 +++++++ tools/nrl_k8s/tests/unit/test_schema.py | 215 +++++ tools/nrl_k8s/tests/unit/test_submit.py | 122 +++ tools/nrl_k8s/tests/unit/test_workdir.py | 108 +++ 35 files changed, 6677 insertions(+) create mode 100644 tools/nrl_k8s/README.md create mode 100644 tools/nrl_k8s/docs/production-readiness.md create mode 100644 tools/nrl_k8s/docs/recipes.md create mode 100644 tools/nrl_k8s/docs/roadmap.md create mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml create mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml create mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml create mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml create mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml create mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_single.yaml create mode 100644 tools/nrl_k8s/pyproject.toml create mode 100644 tools/nrl_k8s/src/nrl_k8s/__init__.py create mode 100644 tools/nrl_k8s/src/nrl_k8s/_logging.py create mode 100644 tools/nrl_k8s/src/nrl_k8s/_retry.py create mode 100644 tools/nrl_k8s/src/nrl_k8s/cli.py create mode 100644 tools/nrl_k8s/src/nrl_k8s/config.py create mode 100644 tools/nrl_k8s/src/nrl_k8s/defaults/defaults.example.yaml create mode 100644 tools/nrl_k8s/src/nrl_k8s/inspect.py create mode 100644 tools/nrl_k8s/src/nrl_k8s/k8s.py create mode 100644 tools/nrl_k8s/src/nrl_k8s/manifest.py create mode 100644 tools/nrl_k8s/src/nrl_k8s/orchestrate.py create mode 100644 tools/nrl_k8s/src/nrl_k8s/schema.py create mode 100644 tools/nrl_k8s/src/nrl_k8s/submit.py create mode 100644 tools/nrl_k8s/src/nrl_k8s/workdir.py create mode 100644 tools/nrl_k8s/tests/__init__.py create mode 100644 tools/nrl_k8s/tests/unit/__init__.py create mode 100644 tools/nrl_k8s/tests/unit/test_cli.py create mode 100644 tools/nrl_k8s/tests/unit/test_config.py create mode 100644 tools/nrl_k8s/tests/unit/test_inspect.py create mode 100644 tools/nrl_k8s/tests/unit/test_k8s.py create mode 100644 tools/nrl_k8s/tests/unit/test_manifest.py create mode 100644 tools/nrl_k8s/tests/unit/test_orchestrate.py create mode 100644 tools/nrl_k8s/tests/unit/test_schema.py create mode 100644 tools/nrl_k8s/tests/unit/test_submit.py create mode 100644 tools/nrl_k8s/tests/unit/test_workdir.py diff --git a/tools/nrl_k8s/README.md b/tools/nrl_k8s/README.md new file mode 100644 index 00000000000..5e81c4c53d6 --- /dev/null +++ b/tools/nrl_k8s/README.md @@ -0,0 +1,426 @@ +# nrl-k8s + +A config-driven launcher that runs a NeMo-RL recipe on any Kubernetes cluster +with the KubeRay operator installed. One YAML pair captures *what to train* +(the recipe) and *where to run it* (the infra); the CLI brings up every +RayCluster, submits any long-running daemons (gen / gym servers), and kicks +off the training job in a single step. + +## Prerequisites + +The CLI delegates to `kubectl`, the Kubernetes Python client, and Ray's Job +Submission SDK. Before your first run make sure the following are in place: + +- **`kubectl`** on your `PATH`, pointed at the cluster you want to deploy to. + `kubectl auth can-i create rayclusters -n ` must return `yes`. +- **KubeRay operator** v1.2+ installed on the target cluster. The CLI applies + `ray.io/v1` `RayCluster` custom resources and polls `.status.state`. +- **Ray dashboard** must be reachable from your laptop for job submission. + `nrl-k8s` opens a port-forward via the `kubectl-ray` plugin (or plain + `kubectl port-forward` as a fallback) — `submit.portForward` picks which. +- **AWS EKS, p5.48xlarge**: use an image that bundles the + `aws-ofi-nccl` plugin (the `nvcr.io/nvidian/nemo-rl:nightly` image shipped + with this repo does). The EFA device plugin must be installed in the + cluster so pods can request `vpc.amazonaws.com/efa`. + +## Install + +`nrl-k8s` installs as a standalone CLI from this repo. Use [uv](https://docs.astral.sh/uv/) +for both setup and development — it's what the project is tested with. + +### End-user install (global `nrl-k8s` binary) + +```bash +uv tool install ./tools/nrl_k8s +nrl-k8s --version +``` + +`uv tool install` drops the CLI in `~/.local/bin` (on `PATH`) inside its +own isolated environment, so it never clashes with whatever your project +venv has pinned. Upgrade with `uv tool upgrade nrl-k8s` after a git pull, +or `uv tool install --reinstall ./tools/nrl_k8s`. + +### Development (editable install + tests) + +```bash +# from the repo root +cd tools/nrl_k8s +uv venv # creates .venv/ +source .venv/bin/activate +uv pip install -e ".[test]" # editable install + test extras +pytest # 9 test modules, ~100 tests +``` + +Or run commands without activating the venv: + +```bash +uv run --directory tools/nrl_k8s -- pytest +uv run --directory tools/nrl_k8s -- nrl-k8s --help +``` + +The package depends on `click`, `omegaconf`, `pydantic`, `kubernetes`, +`ray[default]`, and `tenacity`. It does *not* require the full `nemo_rl` +package to be importable on your laptop — the CLI stages a working_dir for +Ray's Job SDK and runs the training entrypoint inside the cluster image. + +## Quick start + +Three canonical flows ship with working recipes under +`tools/nrl_k8s/examples/`. All three train Qwen3-4B with GRPO on the +`instruction_following` gym; they differ in how many RayClusters the run +occupies and where generation/gym live. + +| variant | RayClusters | generation | gym | +|---|---|---|---| +| `qwen3_4b_if_single` | 1 | colocated in training cluster | local Ray actor in training cluster | +| `qwen3_4b_if_gym_disagg` | 2 | colocated in training cluster | dedicated RayCluster + HTTP daemon | +| `qwen3_4b_if_full_disagg` | 3 | dedicated RayCluster + HTTP daemon | dedicated RayCluster + HTTP daemon | + +### `qwen3_4b_if_single` — everything on one RayCluster + +Simplest shape. One GPU RayCluster hosts training + colocated vLLM, and +nemo_gym runs as a local Ray actor pinned to the worker node. No HTTP +between roles, no endpoint-registry rendezvous. + +```bash +nrl-k8s run \ + tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ + --follow +``` + +### `qwen3_4b_if_gym_disagg` — gym on its own cluster + +One GPU RayCluster hosts training and colocated vLLM; a CPU-only +RayCluster runs the gym rollout server. + +```bash +nrl-k8s run \ + tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml \ + --follow +``` + +`run` applies both RayCluster manifests in order, submits the gym daemon +once its cluster is `Ready`, then submits the training Ray Job against the +training cluster and tails its logs. + +```bash +nrl-k8s status \ + tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml +``` + +### `qwen3_4b_if_full_disagg` — generation + gym + training on separate clusters + +Three RayClusters, one per role. Training streams generation requests to +the standalone generation server, which lives on its own GPUs: + +```bash +nrl-k8s run \ + tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ + --follow +``` + +`run` walks the three roles in order: `generation` first (vLLM has to be +serving before training opens sockets to it), then `gym` (publishes +`gym_head_server` into the endpoint-registry ConfigMap), then `training`. +Once the training Ray Job is submitted its auto-generated ID is printed and +`--follow` tails its logs via a port-forward to the training dashboard. + +```bash +nrl-k8s status \ + tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml +``` + +`DAEMON` is populated for `generation` and `gym`; `training` shows `—` +because its jobs are short-lived and auto-named, so look them up with +`nrl-k8s job list --role training`. + +## Config layout + +Each run is two files: a recipe and an infra. + +- **`.yaml`** — pure NeMo-RL config. Everything the training + entrypoint (`examples/nemo_gym/run_grpo_nemo_gym.py` in the examples) + expects: `policy`, `grpo`, `data`, `logger`, etc. Inherits from + `examples/configs/recipes/**` via a standard `defaults:` field so it stays + short. Portable across clusters. +- **`.infra.yaml`** — K8s-only. Namespace, container image, the + inline RayCluster spec for each role, daemon entrypoints, the training + entrypoint, and where Ray should upload code from. Validated against + `nrl_k8s.schema.InfraConfig` (see `tools/nrl_k8s/src/nrl_k8s/schema.py`). + +You can also bundle the two in one file — put an `infra:` top-level key on +the recipe and omit `--infra`. The split is preferred for anything you plan +to share, because the recipe itself then has no environmental assumptions. + +### `defaults:` inheritance + +Recipes support a `defaults:` field (same semantics as NeMo-RL's own +loader). Point it at a parent recipe path relative to the file itself: + +```yaml +# tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml +defaults: ../../../examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml + +grpo: + max_num_steps: 200 # override one field from the parent +``` + +The parent is loaded first; the child's keys are then merged on top. Chains +work — the parent can itself have a `defaults:`. See +`tools/nrl_k8s/src/nrl_k8s/config.py:165` for the walker. + +Infra files also honour `defaults:` (via the same walker), so a team can +keep a `defaults.infra.yaml` with shared node selectors, image, and +namespace and point each per-run infra at it. + +### Override priority + +Four layers stack low-to-high (last wins): + +1. Shipped defaults: `tools/nrl_k8s/src/nrl_k8s/defaults/defaults.example.yaml` +2. User defaults: `~/.config/nrl-k8s/defaults.yaml` (optional; can be + repointed with `NRL_K8S_DEFAULTS=/path/to/file.yaml`) +3. The infra file (via `--infra`) *or* the recipe's `infra:` block. Not both. +4. Hydra-style CLI overrides: `infra.scheduler.queue=team-a`, + `grpo.max_num_steps=10`. + +`infra.*` overrides target the infra layer; everything else targets the +recipe. See `tools/nrl_k8s/src/nrl_k8s/config.py:102` for the partition +logic. + +## Command reference + +Every command takes the recipe path first, then positional Hydra overrides, +then flags. Pass `--infra ` when recipe and infra are split. + +### `nrl-k8s check` + +Load and validate a recipe/infra pair. Default mode prints a one-page +summary (namespace, image, per-role head/worker sizing, daemon ids, full +training entrypoint body). Pass `-o ` to write the fully-resolved +`InfraConfig` + recipe + rendered RayCluster manifests to disk instead — +the format picks up from the extension (`.yaml` / `.json`). + +```bash +# summary +nrl-k8s check \ + tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml + +# full bundle for diffs / kubectl apply --dry-run piping +nrl-k8s check ... -o /tmp/bundle.yaml +``` + +Replaces the older `validate` + `plan` commands (`validate` stays as a +hidden deprecation alias that routes to `check`). To render just a single +role's manifest, pipe from the bundle file, or use +`nrl-k8s cluster up --dry-run --role `. + +### `nrl-k8s cluster up --role {generation,gym,training}` + +Apply the RayCluster manifest for a role, wait for `state=ready`, and +submit the role's daemon if declared. `--dry-run` prints the exact +manifest that would be applied and exits without hitting the API server: + +```bash +nrl-k8s cluster up \ + tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ + --role training --dry-run +``` + +### `nrl-k8s launch` + +Submit a training Ray Job against an **already-up** training cluster. Does +not bring up generation or gym. Use this when `nrl-k8s run` has already +stood things up and you just want to rerun training after editing code. + +```bash +nrl-k8s launch \ + tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ + --follow --replace +``` + +Flags: `--repo-root ` (defaults to `cwd`; the source tree Ray packages +into `working_dir`), `--follow`, `--replace` (see below). + +### `nrl-k8s run` + +Do the full sequence: apply each RayCluster, submit each role's daemon +(for generation / gym), then submit the training Ray Job. Same flags as +`launch`. Safe to re-run idempotently on healthy clusters — already-running +daemons are skipped unless `--replace` is passed. + +### `nrl-k8s cluster up --role ` + +Apply one RayCluster manifest and, once `Ready`, submit its daemon if the +recipe has one. + +```bash +nrl-k8s cluster up \ + tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ + --role gym +``` + +Flags: `--wait/--no-wait`, `--timeout ` (default 900). + +### `nrl-k8s cluster down` + +Delete a RayCluster by role (resolved from the recipe) or by name. + +```bash +nrl-k8s cluster down \ + tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ + --role gym +``` + +Flags: `--role ` or `--name `, `--wait/--no-wait`. + +### `nrl-k8s cluster list -n ` + +List RayClusters in a namespace with their `.status.state`. + +```bash +nrl-k8s cluster list -n nemo-rl-testing +``` + +### `nrl-k8s status` + +One-line-per-role summary of every cluster in the recipe: RayCluster state, +head pod phase, worker pod phases, daemon submission id and Ray Job status. +See the Quick start output above for the exact format. + +### `nrl-k8s logs --role ` + +Stream logs for a role. With `--source auto` (default) the CLI picks the +daemon's Ray Job when the role has one, else the head pod's container +logs. Override with `--source {daemon,head,worker}`. + +```bash +nrl-k8s logs tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ + --role generation -f --tail 500 +``` + +### `nrl-k8s job list --role ` + +List Ray Jobs currently on the role's RayCluster (via its dashboard). + +```bash +nrl-k8s job list \ + tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ + --role training +``` + +### `nrl-k8s job logs --role ` + +Tail logs for a specific Ray Job submission by id on a role's cluster. +Equivalent to `ray job logs --follow ` with the dashboard port-forward +auto-managed. + +### `nrl-k8s job stop --role ` + +Stop a Ray Job by submission id. Useful for clearing a stuck training job +before a re-run (though `launch --replace` does this automatically). + +### `nrl-k8s dev` / `nrl-k8s dashboard` / `nrl-k8s doctor` + +Not yet implemented — stubs print `not yet implemented (phase: ...)` and +exit `2`. + +## `--replace` semantics + +Both `nrl-k8s launch` and `nrl-k8s run` accept `--replace`. It performs +three idempotency-relevant actions before submitting: + +1. **Endpoint registry reset.** The CLI parses the gym daemon's + `--job-id` flag (see `tools/nrl_k8s/src/nrl_k8s/orchestrate.py:231`) and + deletes the `nemo-rl-endpoints-` ConfigMap. Without this the new + gym or training publishes alongside stale keys from a prior failed run, + and the rendezvous picks up stragglers. See the recipes guide for the + registry's role. +2. **Stop running Ray Jobs.** On every cluster touched, any Ray Job in + state `RUNNING` is stopped and the CLI blocks until it reaches a + terminal state (capped at 60 s). This applies to the daemons + (if their `submissionId` matches) and to every running job on the + training cluster. +3. **Suffix daemon submissionIds.** Ray refuses to reuse a submissionId + after the job has terminated, so `--replace` appends `-` to + `infra.clusters..daemon.submissionId` when resubmitting. Your + configured id stays the same for the *next* run — the suffix only + affects this submission. + +`--replace` does **not** delete RayCluster custom resources; clusters stay +up so a re-run doesn't pay the image pull + pod scheduling cost again. Use +`nrl-k8s cluster down` for that. + +## Troubleshooting + +### Slow `working_dir` upload (or `RuntimeError: size over 100 MiB`) + +Ray's Job SDK caps `working_dir` at 100 MiB. `infra.launch.rayUploadPaths` +(and the per-daemon `rayUploadPaths` on each cluster) exists to narrow what +you ship. The disagg example lists individual files under +`resources_servers/instruction_following/` so the 87 MiB `train.jsonl` +isn't included (see `qwen3_4b_if_full_disagg.infra.yaml:229`). If uploads are slow, +`nrl-k8s validate ... --show-recipe` won't help — instead `ls -lh` the +staged tmpdir by running `nrl-k8s launch --follow` and inspecting the log +line `[training] staging working_dir ...` (look at +`tools/nrl_k8s/src/nrl_k8s/workdir.py` for defaults). + +### "expired token" / Kubernetes SSO errors + +`nrl-k8s` uses the same kubeconfig your `kubectl` does. If you see +`TokenRequest: Unauthorized` mid-run, refresh SSO in a separate shell: + +```bash +aws sso login --profile +kubectl auth whoami +``` + +Then re-run the same command — state lives on the cluster, not on your +laptop, so a re-run on an existing RayCluster just reconnects. + +### Gym daemon stuck on `RUNNING` but training hangs + +Gym's standalone server publishes `gym_head_server` into the +`nemo-rl-endpoints-` ConfigMap, and training publishes +`vllm_base_urls` (disagg writes from the gen server; single-cluster writes +from training itself once colocated vLLM spawns). If either side is stale +from a prior run, the rendezvous deadlocks. Fix: + +```bash +kubectl -n delete configmap nemo-rl-endpoints- +# or simply: nrl-k8s run ... --replace +``` + +### GPU OOM in colocated mode + +Colocated vLLM (single-cluster) shares GPUs with the training backend. +`policy.generation.vllm_cfg.gpu_memory_utilization=0.45` in +`qwen3_4b_if_gym_disagg.yaml` leaves 55 % for training state — if you push +context length or batch size, drop it further (e.g. `0.35`) or halve +`policy.max_total_sequence_length`. The disagg pair doesn't have this +problem because generation lives on its own GPUs. + +Note also that colocated runs are **incompatible with** +`PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True` (vLLM's CuMemAllocator +asserts). The disagg entrypoint sets it, the single-cluster entrypoint +does not — see the comment in `qwen3_4b_if_gym_disagg.infra.yaml:76`. + +### Stale endpoint registry between runs + +Symptoms: a fresh `nrl-k8s run` reports the training job submitted but +immediately logs "connection refused" to a URL that belongs to a pod from +a previous run. Always use `--replace` after a failed run; it wipes the +ConfigMap as described under `--replace` semantics. + diff --git a/tools/nrl_k8s/docs/production-readiness.md b/tools/nrl_k8s/docs/production-readiness.md new file mode 100644 index 00000000000..42fb73b1705 --- /dev/null +++ b/tools/nrl_k8s/docs/production-readiness.md @@ -0,0 +1,134 @@ +# nrl-k8s — Production Readiness Audit + +Scope: read-only audit of `tools/nrl_k8s/` at commit `hemil/k8s-infra`. +The CLI is end-to-end functional on Qwen3-4B disagg and single-cluster runs, and 48 unit tests pass on `config`, `schema`, `manifest`. What follows is a prioritised backlog of gaps that block or jeopardise a production launch. + +Priority rubric: **P0** = blocks production deploy / data-loss / security; **P1** = ship-blocker for v1.0 (usability, reliability under failure); **P2** = post-1.0 polish. + +Legend: each finding lists `path:line -> issue -> fix -> priority -> est. minutes`. + +--- + +## 1. Operational robustness + +The CLI talks to k8s and the Ray job submission API over the network and through a `kubectl port-forward` subprocess, but very little of it is defensive against a flaky network, a re-authenticated kubeconfig, or a dead port-forward. + +- `submit.py:41-82 (dashboard_url)` -> port-forward is launched once and never re-established. A laptop Wi-Fi blip, VPN reconnect, or a kubelet restart kills the forward mid-submit and the Ray SDK call fails with an opaque `ConnectionError` -> wrap the dashboard interaction in a reconnect loop (kill + respawn port-forward, wait for TCP, retry the SDK call on `urllib3.exceptions.NewConnectionError`/`MaxRetryError`) -> **P1** -> 60 +- `submit.py:68-73` -> port-forward's stdout is pipe-buffered and only read after the proc exits (`_wait_for_tcp` only reads on early death), so a kubectl that prints "Handling connection for N" but never establishes the forward looks like "timed out" with no diagnostic -> drain stdout into a rolling buffer in a daemon thread and include last N lines in the `TimeoutError` -> **P1** -> 30 +- `submit.py:60-66` -> `kubectl port-forward` is invoked without `--address 127.0.0.1` or `--context`. On a multi-context kubeconfig (common at NVIDIA) we may forward against the wrong cluster -> pass `--context=$(current-context)` and `--address=127.0.0.1` explicitly; surface the chosen context in log -> **P1** -> 15 +- `k8s.py:116-135 (wait_for_raycluster_ready)` -> polls with a flat `poll_s=5` without exponential backoff and without tolerating transient API errors: a single `ApiException` (500, 503, SSL timeout) propagates out and aborts `run`. Likelihood: high during cluster-autoscaler events -> wrap `get_raycluster` in a retry helper that swallows 429/5xx/`urllib3.exceptions.ProtocolError` up to N tries before re-raising -> **P0** -> 45 +- `k8s.py:28-34 (load_kubeconfig)` -> `@functools.cache`d. If the kubeconfig token expires mid-run (AWS SSO, short-lived OIDC), subsequent API calls fail and there is no re-load path -> drop the cache on 401; or re-call `load_kube_config` on each `ApiException.status == 401` -> **P1** -> 45 +- `k8s.py:46-69 (apply_raycluster)` -> on 409 it unconditionally `PATCH`es even if another user's run owns the RayCluster. There is no owner-label check, no `resourceVersion` race guarantee; two concurrent `run`s can silently overwrite each other's specs -> before patch, verify a `managed-by=nrl-k8s` label and the current user / run-id; error out if the existing cluster was not ours -> **P0** -> 60 +- `orchestrate.py:108, 295, inspect.py:120` -> bare `except Exception` swallow every failure including `KeyboardInterrupt`'s non-`BaseException` cousins and API auth failures; user sees `daemon_status=None` and assumes "not running" when in truth the dashboard is unreachable -> narrow to `(ApiException, JobSubmissionClientError, ConnectionError, TimeoutError)` and surface the reason in the status row -> **P1** -> 30 +- `orchestrate.py:304-316 (_wait_for_http)` -> health-check loop retries every 5s with no backoff; a misconfigured URL (e.g. cluster-internal DNS when the CLI is running on a laptop) silently spins for 5 min -> detect obvious laptop-cannot-reach-cluster-DNS conditions (`socket.gaierror` on `*.svc.cluster.local`) and fail-fast with an actionable message -> **P1** -> 20 +- `orchestrate.py:282-301 (_wait_job_stopped)` -> a Ray Job that Ray insists is `RUNNING` but whose node has been evicted stays `RUNNING` forever; timeout is just 60s and then the loop logs "continuing" and proceeds to submit a clashing daemon -> after timeout, call `client.delete_job` (and error if it still isn't terminal) instead of silently racing -> **P1** -> 30 +- `submit.py:121-146 (tail_job_logs)` -> daemon thread uses a blocking `q.get()` with no timeout, so `Ctrl+C` during a tail will cancel the main thread's sleep but leave the async asyncio loop hanging until the process exits. Usually benign but leaks file descriptors under long-running observability sessions -> use `q.get(timeout=1)` in a loop and wind down the asyncio loop cleanly -> **P2** -> 20 +- `cli.py:82/170/227/432/536/602/619/656` -> every top-level error handler prints `error: {exc}` and calls `sys.exit(1)`. The raw exception from `kubernetes.client.ApiException` includes a multi-line dump with headers, which is noisy; the root cause (e.g. "kubeconfig expired") is buried -> classify exceptions and emit a short, actionable first line (`hint: kubeconfig expired; run aws sso login`) before the full trace under `-v` -> **P1** -> 60 +- `submit.py:179-190 (_wait_for_tcp)` -> polls every 0.5s for 30s; if kubectl spawns but the LB hasn't propagated, you see "didn't open 127.0.0.1:X in 30s" with no hint. Make the timeout configurable via `infra.submit.portForwardTimeoutS` -> **P2** -> 15 +- `orchestrate.py:218-225 (run)` -> clusters are brought up sequentially (generation, then gym, then training). A transient failure on the gym cluster tears the whole run; there is no resumability (the CLI doesn't persist a run-state) -> add a local state file (`~/.cache/nrl-k8s/runs/.json`) that tracks which clusters are up and allows `--resume` -> **P1** -> 120 + +## 2. Secrets handling + +The CLI doesn't explicitly read secrets, but it emits many code paths that dump user-provided YAML, env dicts, or pod manifests. Those payloads frequently contain tokens. + +- `cli.py:86-91 (validate)` -> the resolved `InfraConfig` is printed verbatim. The schema allows `launch.env` / `daemon.env` / `networking.extra_env` as `dict[str,str]`, so any user that puts `{WANDB_API_KEY: xxx}` into the recipe sees it leak into the terminal and into `nrl-k8s validate > out.yaml` commits -> either deny-list known secret-looking keys (`*_API_KEY`, `*_TOKEN`, `*_PASSWORD`) when printing, or redact by default and add `--show-secrets` -> **P0** -> 30 +- `cli.py:117-126 (plan)` -> prints the full RayCluster manifest including any env with `valueFrom.secretKeyRef`. That's only a name-reference and usually fine, but if a user inlined a plaintext secret into `spec.headGroupSpec.template.spec.containers[*].env` it gets dumped. Same redaction pass should apply here -> **P1** -> 15 +- `orchestrate.py:126-148` -> on `--replace` the log emits the daemon submission id but not the env; still, the `submit_ray_job` call sends `env_vars=daemon.env` which travels in cleartext to the Ray dashboard (HTTP, no TLS by default). When the dashboard is behind a LoadBalancer this is a genuine risk -> document the HTTP-vs-HTTPS boundary in README + SECURITY.md; add a `submit.insecureHttpDashboard: bool` guard that refuses non-localhost, non-TLS dashboards for env-bearing jobs -> **P0** -> 60 +- `cli.py:82 / 170 / 227` -> `error: {exc}` stack traces leak kubeconfig auth tokens in some `ApiException` message bodies (`kubernetes` client includes response body by default in `reason`). A traceback on a 401 from an OIDC-authenticated cluster typically includes the bearer token in the `Authorization` header that the client echoes back -> before echoing an `ApiException`, scrub `Authorization:` / `Bearer ` patterns from `exc.body` and `exc.headers` -> **P0** -> 40 +- `workdir.py:53-92 (stage_workdir)` -> no scrubbing of `.env`, `*.pem`, `*.key`, `credentials*`, `id_rsa*`, `secrets.yaml` from the copied tree. If a researcher `cd`s into a repo root that contains a `.env` it is uploaded to GCS as part of the working_dir zip and served from the Ray dashboard -> add those patterns to `_IGNORE_PATTERNS`; add a pre-upload size+names preview (`Uploading 97MB across 12,437 files; first hit: examples/.env`) with opt-in `--yes` -> **P0** -> 30 +- `inspect.py:57-66 (collect_status) / cli.py:263-276 (status)` -> not a direct leak but the status command attaches via `dashboard_url` to three clusters in series, and any port-forward failure logs the `kubectl port-forward` stderr which contains the full kubeconfig path and context. Not a secret by itself, but combined with AWS SSO caches it's fingerprinting data -> redact the kubeconfig path from errors -> **P2** -> 15 + +## 3. Test coverage holes + +Only 3 modules have unit tests (`config`, `schema`, `manifest`, totalling 48 tests). The CLI's orchestration, I/O, and CLI layer are wholly untested. + +- `orchestrate.py` -> zero tests. The highest-value business logic (replace logic, `_infer_disagg_job_id`, ConfigMap reset, sequential cluster bring-up, `_wait_for_http`) runs only in production -> add pytest-mock tests that fake `k8s.*` and `JobSubmissionClient` and cover: daemon skipped when RUNNING without --replace; daemon re-submitted with fresh id when --replace; FAILED daemon errors without --replace; `_wait_for_http` returns on 500 **P0** -> 180 +- `submit.py` -> zero tests. The port-forward lifecycle + `submit_ray_job` are the CLI's only k8s-to-Ray bridge -> add subprocess-mocked tests for `dashboard_url` (in-cluster vs laptop branch, kubectl-missing, early-exit, timeout); add a test that `submit_ray_job` assembles the correct `runtime_env` when `pip=None`, `env_vars=None`, `submission_id=None` -> **P0** -> 90 +- `workdir.py` -> zero tests, but it silently drops `.gitignore` files (a bug-fix encoded as behaviour) and implicitly relies on `.gitignore` stripping to ship training data; **no test pins this invariant** -> add tests: `.gitignore` is dropped; a `data/foo.jsonl` file under a `.gitignore`d path is still present in staging; `extra_files` path collisions are well-defined; missing optional paths are skipped -> **P1** -> 45 +- `inspect.py` -> zero tests. `_latest_daemon_job` branch that strips the timestamp suffix is fragile and untested -> add tests mocking `JobSubmissionClient.list_jobs` with base/suffixed/unrelated ids -> **P1** -> 45 +- `k8s.py` -> zero tests. `apply_raycluster` 409 path and `wait_for_raycluster_ready` timeout path never exercised -> pytest-mock the `CustomObjectsApi` and assert both branches -> **P1** -> 60 +- `cli.py` -> zero tests. Nobody has exercised, e.g., `cluster down --name` vs `--role`, or the invariant that `--infra` and recipe-level `infra:` conflict -> use click's `CliRunner` to exercise each subcommand's argument validation and the `sys.exit(2)` path (without actually touching k8s) -> **P1** -> 120 +- `config.py` -> coverage gap: override partitioning for `+infra.foo=x` / `~infra.foo` (append/remove) is implemented but not tested -> add test cases for `+infra.scheduler.queue=x` and `~infra.scheduler.queue` semantics -> **P2** -> 20 +- `schema.py` -> coverage gap: `LaunchSpec.entrypoint` is `Optional` so the sentinel "must be set for nrl-k8s launch" is enforced only at runtime in `orchestrate.submit_training`. Add a schema-level test that at least one scheduler/queue combo in an attach-mode recipe is accepted, and that `launch.mode=attach` without any of the three attach targets is rejected (exists but no test on mixing with `launch.entrypoint=None`) -> **P2** -> 20 + +## 4. CLI UX gaps + +- `cli.py:237-240 / 351-355 / 365-374` -> `doctor`, `dashboard`, `dev up`, `dev down` are advertised in `--help` but just print `not yet implemented (phase: N)` and exit with 2. This is actively misleading for a v1.0 release -> either hide them behind a `NRL_K8S_SHOW_STUBS=1` env flag, or remove them from the group until implemented -> **P1** -> 15 +- `cli.py:82, 170, 227` -> bare stack traces go to stderr. No `--verbose`/`--quiet`/`-v` flag, no log format toggle -> add `--log-level` at the root group, default INFO, with DEBUG surfacing full tracebacks. Use `logging` instead of `click.echo` for diagnostics -> **P1** -> 60 +- no `--dry-run` anywhere -> `nrl-k8s run --dry-run` should plan all three manifests, print what daemons would be submitted with what entrypoints, and exit 0. This is critical for reviewers who can't actually `kubectl apply` -> **P0** -> 60 +- `submit.py / orchestrate.py` -> no progress indicator on 97 MB working_dir uploads or on the 2-3 min cluster bring-up; users ctrl+C thinking the CLI is hung -> print periodic "still waiting on RayCluster X (elapsed 90s / state=Pending)" lines every 30s in `wait_for_raycluster_ready`; print byte count before calling `submit_job` -> **P1** -> 45 +- `cli.py:150-177 (launch) / 201-234 (run)` -> `--follow` tails training logs, but there is no way to tail a daemon at submit time. When gym fails to come up, the user has to find out post-hoc via `nrl-k8s logs --role gym`. Add `--follow-daemons` that streams all daemon logs in parallel -> **P2** -> 60 +- `cli.py:484-497 (cluster list)` -> the `--namespace` flag ignores recipe; other subcommands all require a recipe for namespace resolution. Inconsistent. Either accept an optional recipe or keep it strictly `--namespace` and document -> **P2** -> 10 +- `cli.py:107-126 (plan)` + `cli.py:65-95 (validate)` -> no `--output` flag to write to a file (users have been piping to `> out.yaml`, which breaks `--show-recipe` two-section output) -> add `-o/--output PATH` -> **P2** -> 15 +- no shell completion (`click`'s `click.shell_completion`) -> a trivial 10-line addition; adds noticeable polish -> **P2** -> 15 +- error hint when `--infra` file and recipe both declare `infra:` at `config.py:202-208` -> the ValueError is fine; surfaces through `cli.py:620`. Consider telling the user which line of the recipe has `infra:` -> **P2** -> 15 + +## 5. Packaging + versioning + +- `pyproject.toml` -> dependencies list open-ended lower bounds but no upper pin; `ray[default]>=2.52` will happily install 3.x breaking changes. Pin to a tested range (`>=2.52,<3`) and bump deliberately -> **P1** -> 15 +- `pyproject.toml` -> no `[project.urls]` (home, issues, changelog). No `classifiers`. No `keywords` -> **P2** -> 10 +- no `CHANGELOG.md` -> add a keep-a-changelog with the current 0.1.0 feature set documented -> **P1** -> 20 +- `__init__.py:3` -> `__version__ = "0.1.0"` is duplicated from `pyproject.toml`. Moving to `importlib.metadata.version("nrl-k8s")` avoids drift -> **P2** -> 10 +- no CI for the package. `tools/nrl_k8s` is not wired into the repo's `.github/workflows/*` that I can see -> add a minimal GHA job that runs `pip install tools/nrl_k8s[test]` and `pytest tools/nrl_k8s/tests/unit` on PRs touching the tool -> **P0** -> 45 +- no entry-point test (`pip install . && nrl-k8s --version`) in CI -> add a `tests/unit/test_entry_point.py` that uses `CliRunner` to invoke `--version` and `--help` -> **P1** -> 15 +- no license headers on source files; pyproject says Apache-2.0 but the Python files have no SPDX line. Repo convention check needed -> **P2** -> 15 +- `orchestration/`, `schedulers/`, `backends/`, `templates/` are empty directories with empty `__init__.py` files. Either populate or remove -> **P2** -> 10 + +## 6. Config validation gaps + +The schema is strict (`extra=forbid`), but several runtime invariants are not expressed in pydantic and blow up downstream only after you bring up the clusters. + +- `schema.py:282-302 (ClusterSpec)` -> `spec` is `dict[str, Any]` — zero validation on the RayCluster body. Missing `headGroupSpec` or containers without a name yield cryptic KubeRay webhook errors only after `nrl-k8s run` applies -> add a light check in `manifest.py` that walks the body and raises a clean error for common mistakes (no containers, no head group, image mentioned but empty string) before the k8s call -> **P1** -> 45 +- `orchestrate.py:228-243 (_infer_disagg_job_id)` -> the invariant that training's `+env.disagg_job_id=` matches gym's `--job-id ` is enforced only by a regex hack on the gym entrypoint and with a best-effort "if we can parse it, else skip ConfigMap delete". A mismatch silently makes gym hang forever because training publishes to a different ConfigMap -> promote `disagg_job_id` to a first-class schema field (`infra.launch.disaggJobId: str | None`) and inject it as an env var into both the gym daemon and the training entrypoint; emit a validator error if gym declares one and training is missing it -> **P0** -> 75 +- `schema.py:192-224 (LaunchSpec/AttachSpec)` -> `launch.attach.training: str | None` vs. `clusters.training.name: str` are both strings and nothing enforces they reference the same RayCluster. Similar for `attach.generation` / `clusters.generation.name`. If they drift, the CLI applies a new RayCluster under `clusters.training.name` but submits the job against a nonexistent cluster name in `attach.training` -> add a `model_validator` on `InfraConfig` that, in `mode=attach`, enforces `attach.` equals `clusters..name` (or is None when the cluster is None) -> **P0** -> 30 +- `schema.py:197-207 (LaunchSpec.entrypoint)` -> the doc-comment says "required for `nrl-k8s launch` / `nrl-k8s run`" but the schema marks it Optional and `orchestrate.submit_training:164-165` throws a runtime ValueError. Users who pass only `cluster up --role generation` legitimately need no entrypoint; but `nrl-k8s run` without an entrypoint should fail at load time -> add a `post-validate` step in `_load_or_exit` (or make it a top-level `run`/`launch` precondition in the CLI) that checks `launch.entrypoint` is non-empty and that `clusters.training` is defined -> **P1** -> 30 +- `schema.py:259-279 (DaemonSpec)` -> `submissionId` is optional; but `orchestrate.submit_daemon:106-107` does `client.get_job_status(daemon.submissionId)` unguarded if it's None, which will raise `TypeError`. Add `@model_validator` to require `submissionId` when parent `launch.peerWatcher=True` or when `nrl-k8s run --replace` is likely -> **P1** -> 20 +- `schema.py:193-195 (AttachSpec)` -> gym may be declared without a training cluster; but `nrl-k8s run` assumes `training` exists. Schema doesn't forbid gym-only runs -> either allow gym-only (document) or validate -> **P2** -> 15 +- `config.py:216-218` -> underscore-prefixed top-level keys (e.g. `_shared: &anchors`) are stripped. Silent. A typo like `_shred` would be silently dropped instead of flagged -> log-warn on strip in debug mode -> **P2** -> 10 +- `schema.py:82` -> `NetworkingSpec.extra_env` is never applied anywhere (grep confirms). Dead config key -> either wire it into manifest/patch or remove -> **P2** -> 20 +- `schema.py:228-246 (ResourcesSpec)` -> declared but never consumed — `manifest.py` does not read it. Another dead knob that will mislead users -> remove or implement before v1.0 -> **P1** -> 30 + +## 7. Multi-environment portability + +The code itself is surprisingly free of AWS-specific strings, but the examples (and by extension the demo path the user follows) bake in assumptions. + +- `examples/qwen3_4b_if_full_disagg.infra.yaml:42, 49, 104-105` -> `vpc.amazonaws.com/efa`, `enp71s0`, `FI_PROVIDER=efa` hardcoded in the only examples. First-run on Azure/GCP/on-prem will fail silently (NCCL falls back to TCP and training slows 100x) with no error -> add a second example (`examples/azure_*.infra.yaml` or `examples/no_efa.infra.yaml`) that doesn't assume EFA, plus a note in README explaining the EFA case -> **P1** -> 45 +- `examples/qwen3_4b_if_full_disagg.infra.yaml:14-17` -> `gpu-wrangler.nvidia.com/lease: nemo-rl-testing` label is NVIDIA-internal. Reused in node-selector + toleration -> move to a top-level alias (`${node.lease}`) and document it as "replace with your own scheduler's pool label" -> **P2** -> 20 +- `schema.py:32-35 (SchedulerKind)` -> enum includes `kai`, `kueue`, `default` but nothing in `manifest.py` or `orchestrate.py` actually reads `infra.scheduler.kind` beyond validation. Researchers set `kind: kai, queue: priority-team` and expect the CLI to patch the `scheduling.run.ai/queue` label onto pods — but it doesn't -> either auto-patch the KAI `scheduling.run.ai/queue` / Kueue `kueue.x-k8s.io/queue-name` labels onto `spec.headGroupSpec.template.metadata.labels`, or document loudly that users must add the label themselves -> **P0** -> 60 +- `workdir.py:33-46 (DEFAULT_RAY_UPLOAD_PATHS)` -> the defaults are NeMo-RL-monorepo-shaped (`3rdparty/Gym-workspace/Gym/...`). A thinned-down NeMo-RL checkout will miss most of these. The `if not src.exists(): continue` at workdir.py:79 makes it silent -> log a warning when a default path is skipped; consider making the default list empty and forcing recipes to declare their paths -> **P1** -> 20 +- `orchestrate.py:252 (cm_name = f"nemo-rl-endpoints-{job_id}")` -> the ConfigMap name prefix is hard-coded to match the server-side code in `nemo_rl.distributed.k8s_endpoint_registry`. If the server-side code ever changes the prefix, `--replace` quietly stops working. Share the prefix in a single source of truth (import from `nemo_rl.distributed.k8s_endpoint_registry` with a fallback for the standalone install path) -> **P1** -> 15 +- `submit.py:29 (DASHBOARD_PORT = 8265)` -> hardcoded. KubeRay convention, fine; but a bespoke operator with `--dashboard-port` different from 8265 can't use the CLI -> plumb through `infra.submit.dashboardPort` -> **P2** -> 15 + +## 8. Concurrency safety + +Two researchers running `nrl-k8s run` or even `status` against the same namespace concurrently is a first-class use case (shared dev namespace is standard), and several paths are not safe. + +- `submit.py:173-176 (_free_port)` -> picks a random free port at `bind(0)`. Two concurrent invocations may race: both get port P, the first `kubectl port-forward` binds, the second sees `bind: address already in use` -> either retry with a fresh port on `OSError: EADDRINUSE` from the port-forward, or keep the socket bound while spawning kubectl and hand off; prefer the retry because `SO_REUSEADDR` doesn't help with kubectl -> **P1** -> 20 +- `orchestrate.py:246-254 (_reset_endpoint_registry)` -> deletes the ConfigMap unconditionally. If User A is running with job-id `foo` and User B does `run --replace` with the same `foo`, User A's training loses its registry mid-run and rendezvous breaks silently -> scope ConfigMap name by user (`nemo-rl-endpoints--`) or add an owner annotation and refuse delete if the annotation doesn't match the current run -> **P0** -> 45 +- `orchestrate.py:71 (apply_raycluster)` + `k8s.py:46-69` -> concurrent applies of the same RayCluster name clobber each other; one wins the patch and the other's topology is lost. See §1 above; same fix (owner-label + resourceVersion) -> **P0** -> (shared) +- `submit.py:103-109 (submit_ray_job)` -> `submission_id` is user-supplied (for daemons); Ray's server rejects a re-used id, which is nice, but two concurrent `run --replace` both compute `_fresh_submission_id` from `time.time()` and can collide at second granularity -> use `time.time_ns()` or append a short random suffix -> **P1** -> 10 +- `workdir.py:74 (mkdtemp)` -> each invocation gets its own dir, so staging is fine; but nothing cleans up old `nrl-k8s-workdir-*` directories. Over weeks of use `/tmp` gets hundreds of 100 MB copies -> add a GC helper invoked at CLI startup that deletes `nrl-k8s-workdir-*` older than 24h -> **P2** -> 20 +- `orchestrate.py:183-194 (submit_training --replace)` -> stops ALL running Ray jobs on the training cluster, not just ones owned by this recipe. If a second user shares the training cluster, their job gets killed by your `--replace` -> filter by `metadata.submission_id` prefix (a per-recipe unique prefix) or by a runtime_env label -> **P0** -> 30 + +--- + +## Top 10 to do first + +1. **P0, 30m** — `§2` Scrub `Authorization: Bearer ...` + redact `*_API_KEY` from error output and `validate` output (`cli.py:82/170`, `cli.py:86-91`, `submit.py:179`). +2. **P0, 30m** — `§2` Add `.env`, `*.pem`, `*.key`, `credentials*`, `id_rsa*` to `_IGNORE_PATTERNS` in `workdir.py:19` and preview the first few staged paths before upload. +3. **P0, 60m** — `§7` / `§5` Auto-patch `scheduling.run.ai/queue` (KAI) / `kueue.x-k8s.io/queue-name` (Kueue) labels onto pod templates when `infra.scheduler.kind` is set — currently the enum exists but is never applied. +4. **P0, 75m** — `§6` Promote `disagg_job_id` to a first-class schema field and inject it into both the gym daemon and training entrypoint; forbid recipes whose gym + training job-ids differ. +5. **P0, 60m** — `§1` Wrap every k8s API call (`get_raycluster`, `list_rayclusters`, `list_namespaced_pod`) in a retry helper that swallows 429/5xx/`ProtocolError` for ~3 tries with exponential backoff. +6. **P0, 60m** — `§1` Owner-label guard in `apply_raycluster`: refuse to patch a RayCluster that lacks `managed-by=nrl-k8s` or whose `nrl-k8s/run-id` differs from the current invocation. +7. **P0, 45m** — `§8` Scope endpoint-registry ConfigMap by user (`nemo-rl-endpoints--`) so concurrent `run --replace` can't trash each other's rendezvous. +8. **P0, 60m** — `§4` `--dry-run` on `run` / `launch` / `cluster up`: render + print all manifests and daemon/entrypoint commands without touching k8s. +9. **P0, 180m** — `§3` Tests for `orchestrate.py` covering `submit_daemon --replace`, `--no-replace` FAILED handling, and `_reset_endpoint_registry`. +10. **P0, 45m** — `§5` Add a GitHub Actions job that runs `pip install tools/nrl_k8s[test] && pytest tools/nrl_k8s/tests/unit` on every PR touching the tool, plus a smoke-test entry-point check. + +## Running totals by priority + +- P0 findings: 14 (est. 775 min / 12.9 h) +- P1 findings: 31 (est. 1305 min / 21.75 h) +- P2 findings: 17 (est. 305 min / 5.1 h) + +Overall, ~39 engineering hours to reach a credible v1.0. The P0 list alone is under 2 working days and closes every data-loss / secret-leak / cross-user safety gap that the current codebase has. diff --git a/tools/nrl_k8s/docs/recipes.md b/tools/nrl_k8s/docs/recipes.md new file mode 100644 index 00000000000..0a2bfbc9381 --- /dev/null +++ b/tools/nrl_k8s/docs/recipes.md @@ -0,0 +1,419 @@ +# Writing `nrl-k8s` recipes + +A `nrl-k8s` run is two YAML files. Together they tell the CLI +*what* to train (the recipe) and *where* to run it (the infra). This guide +covers the split, every infra field, and how to port a recipe from one +cluster to another. For the CLI command surface, see the README. + +## Recipe vs. infra + +### Recipe file (`.yaml`) + +Pure NeMo-RL config — exactly what a training entrypoint like +`examples/nemo_gym/run_grpo_nemo_gym.py` expects. Typical keys: `cluster`, +`policy`, `grpo`, `data`, `logger`, `checkpointing`. Inherits from a +parent recipe via `defaults:`. Nothing K8s-specific lives here. + +```yaml +# tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml +defaults: ../../../examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml + +cluster: + gpus_per_node: 8 + num_nodes: 1 +grpo: + num_prompts_per_step: 32 + max_num_steps: 200 +policy: + train_global_batch_size: 512 + max_total_sequence_length: 16384 +``` + +The CLI stages the *merged* recipe (after `defaults:` resolution) as +`nrl_k8s_run.yaml` at the `working_dir` root before submitting the job, so +the entrypoint can reference it by the constant name +(`--config nrl_k8s_run.yaml`). + +### Infra file (`.infra.yaml`) + +K8s-only — the pydantic `InfraConfig` body, defined in +`tools/nrl_k8s/src/nrl_k8s/schema.py`. The recipe and infra files are +loaded independently and merged by the CLI. + +```yaml +# tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml (excerpt) +namespace: nemo-rl-testing +image: nvcr.io/nvidian/nemo-rl:nightly +imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] +serviceAccount: nemo-rl-endpoint-registry +launch: + mode: attach + ... +clusters: + generation: { ... } + gym: { ... } + training: { ... } +``` + +You can also put infra into the recipe as a top-level `infra:` key and omit +`--infra`. Don't do both — the loader refuses +(`tools/nrl_k8s/src/nrl_k8s/config.py:202`). + +## Infra fields + +Every field in the sections below corresponds to a pydantic model in +`schema.py`. The source is the authoritative reference; what follows is +grouped by concern with one example per area. + +### Namespace, image, pull secrets + +```yaml +namespace: nemo-rl-testing # required +image: nvcr.io/nvidian/nemo-rl:nightly # required; patched onto every container +imagePullSecrets: [my-registry-secret] # attached to every pod template +rayVersion: "2.52.0" # optional — defaults to the image default +serviceAccount: nemo-rl-endpoint-registry # set per pod when non-null +labels: {team: nemo-rl} # merged into every RayCluster's metadata +annotations: {} +``` + +`image` is the one field you'll change most often when moving between +clusters. `serviceAccount` is required when anything in the run talks to the +Kubernetes API (e.g. `K8sEndpointRegistry` publishing into a ConfigMap, +used by gym/training rendezvous). + +### Scheduler + +```yaml +scheduler: + kind: kai # "kai" | "kueue" | "default" + queue: priority-team # required when kind != "default" +``` + +`kai`/`kueue` trigger a scheduler-specific annotation/label patch on each +RayCluster. `default` leaves the Kubernetes default scheduler alone. + +### Placement + +```yaml +placement: + nodeSelector: + gpu-wrangler.nvidia.com/lease: nemo-rl-testing + tolerations: + - {key: platform.nvidia.com/gpu, operator: Equal, value: "true", effect: NoSchedule} + affinity: null # rare — raw-dict passthrough to pod spec +``` + +Node selectors and tolerations here apply to every pod template; the +examples duplicate them via YAML anchors inside each `clusters..spec` +for per-cluster flexibility. + +### Networking + +```yaml +networking: + hostNetwork: false # see note under the worked example below + gloo_socket_ifname: enp71s0 + nccl_socket_ifname: enp71s0 + nccl_ib_disable: false + nccl_net: OFI # "Socket" | "IB" | "OFI" + extra_env: {FI_PROVIDER: efa} +``` + +These become pod-level env vars on containers the CLI templates. Most real +recipes leave `networking:` at its defaults and bake the env into the +container image instead — setting them in pod env *and* `launch.env` at the +same time triggers Ray's runtime-env merge conflict (see the inline comments +in `qwen3_4b_if_full_disagg.infra.yaml`). + +### Workspace, HF cache, checkpoints + +```yaml +workspace: + kind: rayUpload # "lustre" | "pvc" | "hostPath" | "rayUpload" | "auto" + pvcName: null # required when kind in {lustre, pvc} + mountPath: /mnt/nemo-rl + repoSubdir: workdirs + size: null # only used when kind=lustre and PVC needs creating + hostPath: null # kind=hostPath only (dev/kind only) +hf_cache: + kind: none # "lustre" | "pvc" | "emptyDir" | "none" + pvcName: null + mountPath: /root/.cache/huggingface +checkpoints: + kind: none # "lustre" | "pvc" | "none" + pvcName: null + mountPath: /mnt/nemo-rl/checkpoints +``` + +`workspace.kind=rayUpload` is the default and the one the shipped examples +use — the CLI packages code into a tmpdir and Ray uploads it to the +cluster's GCS. PVC-backed kinds exist for larger repos or when you want +checkpoints to persist beyond the RayCluster lifetime; today they're +wired at the manifest level but not exercised by the example recipes. + +### Submit (how the CLI gets a job in) + +```yaml +submit: + kind: sdk # "sdk" (Ray Job SDK) | "rayjob" (RayJob CRD) + portForward: auto # "kubectl-ray-plugin" | "kubectl-port-forward" | "auto" + devPod: auto # "auto" | "required" | "skip" (not yet wired) + localDashboardPort: 18265 # avoids collision with `kubectl-ray session` +``` + +### Launch + +```yaml +launch: + mode: attach # "single" | "rayjob" | "attach" | "bringup" + attach: + generation: raycluster-generation-qwen3-4b + gym: raycluster-gym-qwen3-4b + training: raycluster-rl-qwen3-4b + peerWatcher: false # inject the peer-watcher sidecar for failure cascades + entrypoint: | # the training command; see below + export ... + python -u examples/nemo_gym/run_grpo_nemo_gym.py --config nrl_k8s_run.yaml ... + env: {} # runtime_env.env_vars for the training job + rayUploadPaths: # repo-relative paths to include in working_dir + - nemo_rl + - examples + - infra/examples + - 3rdparty/Gym-workspace/Gym/nemo_gym + - ... +``` + +`entrypoint` is required for `launch` / `run`. The CLI stages the merged +recipe as `nrl_k8s_run.yaml` at the `working_dir` root; the entrypoint +references it by that fixed name. + +Keep `rayUploadPaths` narrow — Ray's Job SDK caps the upload at 100 MiB. +List individual files inside heavy directories (see the example gym config) +when a full tree would include unneeded data. `null` means "use the built-in +default" from `tools/nrl_k8s/src/nrl_k8s/workdir.py`. + +### Clusters and daemons + +```yaml +clusters: + generation: + name: raycluster-generation-qwen3-4b + labels: {disagg.nemo-rl/cluster: generation-qwen3-4b} + annotations: {} + daemon: + submissionId: qwen3-4b-generation-server-v7 + entrypoint: | + python -u examples/run_standalone_generation_server.py ... + env: {} # runtime_env.env_vars for the daemon job + healthCheckUrl: null # optional — CLI polls before returning + healthCheckTimeoutS: 300 + rayUploadPaths: [nemo_rl, examples, infra/examples] + spec: + # Full RayCluster .spec — headGroupSpec, workerGroupSpecs, etc. + # Free-form dict; no pydantic schema. + rayVersion: "2.52.0" + headGroupSpec: { ... } + workerGroupSpecs: [ ... ] + gym: { ... } + training: { ... } +``` + +`spec` is the RayCluster `.spec` body passed straight through to the +Kubernetes API, wrapped by the CLI in the `apiVersion: ray.io/v1` + +`kind: RayCluster` + `metadata` envelope. Cross-cutting fields (`image`, +`imagePullSecrets`, `serviceAccountName`) are patched from the top-level +keys, so you don't repeat them. See +`tools/nrl_k8s/src/nrl_k8s/manifest.py`. + +### Resources + +```yaml +resources: + training: + head: {cpu: "8", memory: "32Gi", gpu: null} + worker: {cpu: "96", memory: "768Gi", gpu: 8} + generation: { ... } + gym: { ... } +``` + +The `resources:` block is an escape hatch for when you want the CLI to +derive sensible container `resources:` per role instead of specifying +them inline in `spec`. The shipped examples set container resources +directly inside `spec` (via YAML anchors) for maximum clarity, so the +`resources:` block stays at defaults. + +## Writing a fresh recipe from scratch + +Suppose you want to run a simple colocated SFT-like scenario: one GPU +RayCluster, one training Ray Job, no gym, no generation server. Here's the +minimum pair. + +### Recipe (`examples/sft_llama3_1b.yaml`) + +```yaml +defaults: ../../../examples/configs/sft_llama3.1_1b.yaml + +cluster: + gpus_per_node: 8 + num_nodes: 1 +policy: + train_global_batch_size: 128 + max_total_sequence_length: 4096 +checkpointing: + save_period: 500 +logger: + wandb_enabled: true + wandb: {entity: nvidia, project: nrl-k8s-smoke, name: sft-llama3-1b} +``` + +### Infra (`examples/sft_llama3_1b.infra.yaml`) + +```yaml +namespace: nemo-rl-testing +image: nvcr.io/nvidian/nemo-rl:nightly +imagePullSecrets: [nvidia-ngcuser-pull-secret] +serviceAccount: nemo-rl-endpoint-registry + +launch: + mode: attach + attach: + training: raycluster-sft-llama3-1b + peerWatcher: false + rayUploadPaths: + - nemo_rl + - examples + - infra/examples + entrypoint: | + export GLOO_SOCKET_IFNAME=enp71s0 + export NCCL_SOCKET_IFNAME=enp71s0 + export FI_PROVIDER=efa + python -u examples/run_sft.py --config nrl_k8s_run.yaml + +clusters: + training: + name: raycluster-sft-llama3-1b + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0"} + template: + spec: + nodeSelector: {gpu-wrangler.nvidia.com/lease: nemo-rl-testing} + tolerations: + - {key: gpu-wrangler.nvidia.com/lease, operator: Equal, value: nemo-rl-testing, effect: NoSchedule} + containers: + - name: ray-head + resources: {limits: {cpu: "8", memory: "32Gi"}} + ports: + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: {num-gpus: "8"} + template: + spec: + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: {gpu-wrangler.nvidia.com/lease: nemo-rl-testing} + tolerations: + - {key: gpu-wrangler.nvidia.com/lease, operator: Equal, value: nemo-rl-testing, effect: NoSchedule} + containers: + - name: ray-worker + resources: + limits: {cpu: "96", memory: "768Gi", nvidia.com/gpu: "8", vpc.amazonaws.com/efa: "32"} +``` + +Launch: + +```bash +nrl-k8s run examples/sft_llama3_1b.yaml \ + --infra examples/sft_llama3_1b.infra.yaml --follow +``` + +Note what we did *not* declare: no `generation:`, no `gym:`, no daemon, +no `launch.env`. The `run` loop skips undeclared roles (see +`orchestrate.py:218`) so a training-only recipe flows straight through. + +## Adapting an existing recipe to a new cluster + +Assume you want to take `qwen3_4b_if_full_disagg.yaml` + `.infra.yaml` and run them on +a different cluster. Typical changes, in order of likelihood: + +1. **`namespace`** — the one you have permission in. +2. **`image`** and **`imagePullSecrets`** — point at the registry + reachable from the new cluster. The image must ship the NCCL/EFA plugin + your network needs. +3. **`serviceAccount`** — the SA that can read/write + `nemo-rl-endpoints-*` ConfigMaps in the new namespace. +4. **`placement.nodeSelector`** and **`tolerations`** — match your node + labels. In the shipped example these are inlined inside each + `clusters..spec` via anchors, so update both places. +5. **Worker resources** — change + `clusters..spec.workerGroupSpecs[].template.spec.containers[].resources` + to match your instance type. On non-EFA clusters, drop + `vpc.amazonaws.com/efa` and set `hostNetwork: false`. +6. **Environment variables** in the `entrypoint` blocks — + `GLOO_SOCKET_IFNAME`, `NCCL_SOCKET_IFNAME`, `FI_PROVIDER` are specific + to AWS p5.48xlarge. On GCP A3 they're different; on baremetal, drop + them entirely if the image already sets sensible defaults. +7. **`launch.attach.*` names** and **`clusters..name`** — bump these + if you're running the same recipe side-by-side with an existing run + (e.g. append `-v2`). The `submissionId` of each daemon should change too + so Ray accepts the new submission. + +Things that should *not* change when porting: + +- The recipe file. The recipe is cluster-agnostic by design; if you're + editing `policy:`, `grpo:`, etc. to port it, something's off. +- The training `entrypoint`'s python command. Env vars and data paths may + differ; the `python -u examples/.../entry.py --config nrl_k8s_run.yaml + ...` line stays. +- `launch.rayUploadPaths` — governed by what the entrypoint imports, not + by the target cluster. + +## Endpoint registry ConfigMap + +Disaggregated and single-cluster-colocated runs both need the gym and +training halves to find each other. They rendezvous via a Kubernetes +ConfigMap named `nemo-rl-endpoints-` in the namespace. Keys: + +- `gym_head_server` — published by the gym standalone server once its HTTP + listener is up. +- `vllm_base_urls` — published by the generation side (the standalone + gen server in disagg mode; colocated vLLM in single-cluster mode) so + training and gym know where to send generation requests. + +`` comes from the `--job-id` flag on the gym entrypoint (see +`qwen3_4b_if_full_disagg.infra.yaml:221`). The CLI does not have a separate config key +for it — that would just duplicate what's in the entrypoint. + +### How `disagg_job_id` is inferred + +`--replace` wipes the ConfigMap so a new run rendezvouses on fresh keys. +The CLI finds the ConfigMap's name by parsing `--job-id ` from the +**gym daemon entrypoint** string +(`tools/nrl_k8s/src/nrl_k8s/orchestrate.py:228`). If your recipe has no +gym cluster or the gym daemon entrypoint doesn't include `--job-id`, the +reset is skipped silently. Keep the flag on one line and quote-free for +the regex (`--job-id my-id` or `--job-id=my-id`). + +The same `job_id` is fed into the training entrypoint via +`+env.disagg_job_id=` (see `qwen3_4b_if_full_disagg.infra.yaml:116`) so training's +`K8sEndpointRegistry` publishes and reads from the same ConfigMap. + +### What `--replace` cleans up + +As covered in the README: + +1. `nemo-rl-endpoints-` ConfigMap deleted. +2. Running Ray Jobs on any touched cluster stopped (daemons and training). +3. Daemon `submissionId` suffixed with a unix timestamp for the + resubmission — Ray can't reuse IDs even after a terminal state. + +It does *not* delete RayClusters or PVCs; use `nrl-k8s cluster down` for +that. diff --git a/tools/nrl_k8s/docs/roadmap.md b/tools/nrl_k8s/docs/roadmap.md new file mode 100644 index 00000000000..631259cc0d5 --- /dev/null +++ b/tools/nrl_k8s/docs/roadmap.md @@ -0,0 +1,111 @@ +# `nrl-k8s` roadmap + +Features that were scaffolded and then removed from the CLI surface pending +real implementations. Re-add the commands as they land so `--help` only +advertises things that work. + +## `doctor` — cluster-baseline health check + +Currently removed. Intended behavior: + +- confirm KubeRay operator ≥ v1.5 is installed and the `RayCluster` CRD is + reachable +- if `infra.scheduler.kind=kai`, confirm the KAI scheduler deployment is + healthy and the `Queue` CRD exists +- if `infra.scheduler.kind=kueue`, confirm the `ClusterQueue` CRD exists +- confirm the configured `serviceAccount` exists in `infra.namespace` +- confirm at least one node advertises `nvidia.com/gpu` in allocatable +- on AWS: confirm the EFA device plugin exposes `vpc.amazonaws.com/efa` + +Exits non-zero on any failed check with a remediation hint per failure. +Good fit for CI preflight and for the `--hint` path of `_explain_and_exit`. + +## `dashboard ` — open the Ray dashboard locally + +Port-forward a RayCluster's head service to a local port and open the URL +(via `webbrowser.open`) in the default browser. Uses the existing +`submit.dashboard_url` context manager. Removed because the happy path is +already covered by `nrl-k8s logs --role training --source daemon` and +`nrl-k8s job list --role …`, but worth re-adding for the "eyeball metrics" +use case. + +## `dev up` / `dev down` — in-cluster dev pod + +Long-running `nrl-dev-` Pod per researcher (one-time create). Image +matches the RayCluster container image so `ray.job_submission.JobSubmission +Client` upload+client Ray versions are identical. Mounts the shared PVC for +code edits and the HF cache PVC. ServiceAccount limited to the minimum +needed to `create` RayClusters + get/list/watch pods + configmaps in +`infra.namespace`. + +Workflow from a laptop becomes: + +```bash +nrl-k8s dev up # one-time +kubectl exec -it pod/nrl-dev-$USER -- bash +# inside: +nrl-k8s run tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml +``` + +Main benefits: no port-forward (the pod is in-cluster so it uses CNI DNS +to reach the Ray dashboard), no laptop-speed upload of the 100 MiB working +dir, and nothing breaks when SSO tokens expire in the middle of a run. + +Removed because the feature requires a shared PVC provisioned per +researcher + RBAC templates; that's a separate platform task. + +## `job list` auto-lookup for training submissions + +Today `job list --role training` returns zero rows because training jobs +have auto-generated submission IDs and there's no stable identifier on +the cluster to correlate a run. A future enhancement tags each submission +with a `wandbRunId` annotation (via `runtime_env.metadata`) so `job list` +can render one line per run with the wandb URL inline. + +## Multi-context support + +`load_kubeconfig` is cached per process; to support running against two +clusters from one shell session (e.g. staging + prod), surface +`--context ` on the root group and thread it down into every +`custom_objects_api()` / `CoreV1Api()` call site. + +## RayJob CRD submission path + +`infra.launch.mode=rayjob` is declared in the schema but `orchestrate.py` +always follows the SDK path. Adding the CRD path means rendering a +`RayJob` manifest with `clusterSelector: {ray.io/cluster: }` and +`submissionMode: HTTPMode`, then applying it via the same manifest builder +used for `RayCluster`. Useful for GitOps pipelines where durable, +k8s-native job state is required. Out of scope for v1.0 unless a caller +shows up for it. + +## Pluggable backends, schedulers, orchestration + +The `backends/`, `schedulers/`, and `orchestration/` subpackages were +sketched early and deleted after a few iterations — none had callers and +the orchestration logic consolidated into a single `orchestrate.py`. Keep +this list so the scaffolding doesn't come back reflexively: + +- **Backends** (`backends/base.py:LaunchBackend`, `kuberay.py`, + `jobset.py`) — abstract layer for non-Ray workloads (JobSet-based pure + PyTorch). Current code assumes KubeRay + `JobSubmissionClient`. If a + JobSet path is needed, add one backend module that implements the same + shape (`apply_cluster`, `submit_job`, `wait_ready`) and flip based on an + `infra.launch.backend: kuberay|jobset` field. +- **Schedulers** (`schedulers/kai.py`, `kueue.py`, `default.py`) — per- + scheduler manifest mutators. `infra.scheduler.kind=kai|kueue` is parsed + into `SchedulerSpec` today but nothing patches the resulting manifest + (no `kai.scheduler/queue` label, no `kueue.x-k8s.io/queue-name`, no + `spec.suspend`). When re-added, wire into `manifest.build_raycluster + _manifest` as a post-patch step keyed on `infra.scheduler.kind`. +- **Orchestration modes** (`orchestration/single.py`, `disagg.py`) — + `LaunchMode` declares `single`, `rayjob`, `attach`, `bringup` but the + code branches only on `.clusters.` presence. If we grow a CRD- + based or bring-up-only path, split the driver in `orchestrate.py` by + mode rather than resurrecting the subpackage. + +The empty `templates/` dir was also removed — the CLI renders RayCluster +objects by patching a Python dict (see `manifest.build_raycluster_ +manifest`). Jinja stays off the table unless the generated YAML stops +fitting into that pattern. diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml new file mode 100644 index 00000000000..b0c06dd75b8 --- /dev/null +++ b/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml @@ -0,0 +1,383 @@ +# Infra for the Qwen3-4B full-disaggregated run — paired with qwen3_4b_if_full_disagg.yaml. +# +# Usage: +# nrl-k8s run tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml +# +# Everything K8s-specific lives here so the recipe stays cluster-agnostic. +# Shared YAML anchors below keep the three cluster specs from repeating. + +# ============================================================================= +# Shared anchors — consumed during YAML parse. +# ============================================================================= +_shared: + nodeSelector: &shared_node_selector + gpu-wrangler.nvidia.com/lease: nemo-rl-testing + tolerations: &shared_tolerations + - {key: gpu-wrangler.nvidia.com/lease, operator: Equal, value: nemo-rl-testing, effect: NoSchedule} + - {key: platform.nvidia.com/gpu, operator: Equal, value: "true", effect: NoSchedule} + # Keep pod-spec env minimal — the nemo-rl:nightly image already sets + # NCCL_NET_PLUGIN=aws-ofi, NCCL_SOCKET_IFNAME=enp71s0, etc. in its shell + # init. Overriding them here just causes Ray runtime-env merge conflicts + # when ray.init() captures os.environ inside the job entrypoint. + headEnv: &shared_head_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + workerEnv: &shared_worker_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + headResources: &shared_head_resources + limits: {cpu: "8", memory: "32Gi"} + requests: {cpu: "2", memory: "8Gi"} + gpuWorkerResources: &gpu_worker_resources + # Sized for p5.48xlarge (192 CPU / ~1957 GiB allocatable). We claim + # most of the node and leave ~16 CPU + ~150 GiB headroom for: + # - the training head pod colocated here (~8 CPU / 32 GiB) + # - a gym head pod if it lands on this node (~8 CPU / 32 GiB) + # - daemonsets: kube-proxy, aws-node, nvidia-device-plugin, + # node-exporter (~4 CPU / 16 GiB total) + # p5.48xlarge also exposes 32 EFA-capable ENIs. The AWS OFI NCCL plugin + # (bundled in the image as NCCL_NET_PLUGIN=aws-ofi) needs them mapped + # into the container as `vpc.amazonaws.com/efa` resources — the EFA + # device plugin on the node injects the character devices under + # /dev/infiniband/ and the libfabric provider discovers them at + # startup. Drop the EFA request and you'll see `FI_PROVIDER=efa` log + # "No usable providers found" and NCCL fall back to socket / hang. + # Also: `nvidia.com/gpu: "8"` must equal the rayStartParams num-gpus + # on the same worker template, otherwise Ray's resource view disagrees + # with what CUDA actually sees. + limits: + cpu: "176" + memory: "1800Gi" + nvidia.com/gpu: "8" + vpc.amazonaws.com/efa: "32" + requests: + cpu: "176" + memory: "1800Gi" + nvidia.com/gpu: "8" + vpc.amazonaws.com/efa: "32" + gpuHeadPorts: &gpu_head_ports + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + dshmMounts: &dshm_mounts + - {mountPath: /dev/shm, name: dshm} + dshm16: &dshm_16 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 16Gi} + dshm64: &dshm_64 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 64Gi} + dshm4: &dshm_4 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 4Gi} + +# ============================================================================= +# Top-level InfraConfig (no wrapping `infra:` needed — the CLI accepts either) +# ============================================================================= +namespace: nemo-rl-testing +image: nvcr.io/nvidian/nemo-rl:nightly +imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] +serviceAccount: nemo-rl-endpoint-registry + +launch: + mode: attach + attach: + generation: raycluster-generation-qwen3-4b + gym: raycluster-gym-qwen3-4b + training: raycluster-rl-qwen3-4b + peerWatcher: false + # Minimal upload set — Ray caps working_dir at 100 MiB. We include only + # what this run needs: nemo_rl, examples, the nemo_gym subset, and the + # instruction_following data (~90 MiB, fits under the cap). + rayUploadPaths: + - nemo_rl + - examples + - infra/examples + - tests/check_metrics.py + - tests/json_dump_tb_logs.py + - 3rdparty/Gym-workspace/Gym/nemo_gym + - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model + - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent + - 3rdparty/Gym-workspace/Gym/pyproject.toml + - 3rdparty/Gym-workspace/Gym/uv.lock + # Only configs (not data/*.jsonl) — the big train/validation jsonls are + # pre-staged via kubectl cp onto the training pods' /tmp (see env above). + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs + # nemo_rl.distributed.model_utils imports megatron.core. + - 3rdparty/Megatron-LM-workspace/Megatron-LM/megatron + # The CLI stages the merged recipe as ./nrl_k8s_run.yaml at the working_dir + # root — reference it by name from the entrypoint. + entrypoint: | + # kubernetes client is required by K8sEndpointRegistry (used by training + # to read vllm_base_urls from the ConfigMap). The image *usually* has it, + # but installing here is cheap insurance and keeps the recipe portable. + pip install kubernetes -q || true + # Inline exports — see note on launch.env below. + # GLOO_SOCKET_IFNAME: PyTorch distributed gloo backend (used for the + # rendezvous store). Default eth0 isn't the interface on p5 hosts. + export GLOO_SOCKET_IFNAME=enp71s0 + # NCCL_SOCKET_IFNAME: control-plane interface NCCL uses before OFI + # takes over data. Must match the host's primary AWS VPC ENI. + export NCCL_SOCKET_IFNAME=enp71s0 + # FI_PROVIDER: tells libfabric (and thus aws-ofi-nccl) to use EFA. + # Without it you'd get the TCP provider over eth0, ~10x slower. + export FI_PROVIDER=efa + # Data paths — files were pre-staged on the pods via kubectl cp onto + # each training pod's /tmp (the 87 MB jsonl blows the 100 MiB + # working_dir cap, so Ray can't ship it). + export DISAGG_TRAIN_PATH=/tmp/train.jsonl + export DISAGG_VALID_PATH=/tmp/validation.jsonl + # expandable_segments reduces CUDA memory fragmentation under varying + # batch shapes. Safe to set in disagg mode; UNSAFE in colocated mode + # (vLLM's CuMemAllocator asserts). The single-cluster recipe omits it. + export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True + # Kill Ray tasks that can never schedule instead of letting them + # queue forever — surfaces cluster-capacity bugs fast. + export RAY_enable_infeasible_task_early_exit=true + # Make sibling workspaces importable alongside the repo root. + export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM + # disagg_job_id is passed in three places — here (training), + # the gen daemon export DISAGG_JOB_ID (generation), and the gym daemon's + # --job-id flag (gym). It is deliberately NOT an InfraConfig field: the + # CLI infers the ConfigMap name by regex-parsing --job-id out of the gym + # entrypoint (orchestrate.py:_infer_disagg_job_id), so adding a separate + # key would just duplicate information and let the two go out of sync. + # Keep all three spellings in sync when you rename a run. + python -u examples/nemo_gym/run_grpo_nemo_gym.py \ + --config nrl_k8s_run.yaml \ + +policy.generation.remote_generation_url=http://raycluster-generation-qwen3-4b-head-svc.nemo-rl-testing.svc.cluster.local:8089 \ + +env.disagg_job_id=qwen3-4b-if-gym \ + ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ + ~policy.optimizer.kwargs.foreach \ + ~policy.optimizer.kwargs.fused + # All env flows through inline `export`s in the entrypoint — setting + # anything here puts it in runtime_env.env_vars, which can't merge with + # ray.init's captured env_vars and aborts with "Failed to merge". + env: {} + +clusters: + # ------------------------------------------------------------------------- + # Generation — GPU head + worker, control-server on 8089. + # ------------------------------------------------------------------------- + generation: + name: raycluster-generation-qwen3-4b + labels: + disagg.nemo-rl/cluster: generation-qwen3-4b + disagg.nemo-rl/run: qwen3-4b-gym + daemon: + submissionId: qwen3-4b-generation-server-v7 + # Inline exports (not runtime_env.env_vars, which conflicts with + # ray.init's captured env). The Python process calls ray.init() after + # these are exported, ray captures them into its env, and propagates + # them to the worker actors + their subprocesses — so vLLM's + # EngineCore sees GLOO_SOCKET_IFNAME=enp71s0 and doesn't try eth0. + # DISAGG_JOB_ID triggers the K8sEndpointRegistry publish path so + # gym can discover the gen shards via the ConfigMap. + entrypoint: | + export GLOO_SOCKET_IFNAME=enp71s0 + export NCCL_SOCKET_IFNAME=enp71s0 + export FI_PROVIDER=efa + export DISAGG_JOB_ID=qwen3-4b-if-gym + # K8sEndpointRegistry needs the `kubernetes` client. + pip install kubernetes -q || true + python -u examples/run_standalone_generation_server.py \ + --config infra/examples/generation_standalone_qwen3_4b_dapo.yaml \ + --port 8089 --num-gpus 8 + # Gen server doesn't need training data — keep the upload small. + rayUploadPaths: + - nemo_rl + - examples + - infra/examples + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-head + env: *shared_head_env + resources: *shared_head_resources + ports: + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + - {containerPort: 8089, name: control-server} + volumeMounts: *dshm_mounts + volumes: *dshm_16 + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: {num-gpus: "8", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + # hostNetwork=true on GPU workers is required for EFA: the + # AWS OFI NCCL plugin opens raw libfabric endpoints bound to + # the host ENI (enp71s0, above), which a pod network + # namespace can't see. With the default CNI network the + # plugin initialises but communication never completes and + # NCCL hangs at the first AllReduce. `dnsPolicy: + # ClusterFirstWithHostNet` then restores in-cluster DNS so + # the worker can still resolve the head svc and the gen + # server's FQDN. + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-worker + env: *shared_worker_env + resources: *gpu_worker_resources + volumeMounts: *dshm_mounts + volumes: *dshm_64 + + # ------------------------------------------------------------------------- + # Gym — CPU-only head, gym-server on 9090. + # ------------------------------------------------------------------------- + gym: + name: raycluster-gym-qwen3-4b + daemon: + submissionId: qwen3-4b-if-gym-server-v7 + # Ray runs entrypoints under /bin/dash. We stay POSIX here (no + # pipefail, no process substitution). OmegaConf ${...} escaped as + # \${...} — Python dollar-refs for ray.__version__ use a one-liner. + entrypoint: | + set -eu + ROOT_DIR=$(pwd) + GYM_DIR=\${ROOT_DIR}/3rdparty/Gym-workspace/Gym + export PYTHONPATH=\${ROOT_DIR}:\${GYM_DIR}:\${PYTHONPATH-} + cd \${GYM_DIR} + mkdir -p cache + RAY_VERSION=$(python -c "import ray; print(ray.__version__)") + echo "ray==\${RAY_VERSION}" > /tmp/ray-pin.txt + pip install -e "." --constraint /tmp/ray-pin.txt -q + pip install kubernetes -q + exec python -m nemo_gym.standalone_server \ + --job-id qwen3-4b-if-gym \ + --port 9090 \ + --model-name Qwen/Qwen3-4B-Instruct-2507 \ + --config-yaml \${ROOT_DIR}/infra/examples/gym_standalone_config_instruction_following.yaml + # Gym needs Gym code + infra configs + the nemo_rl.distributed package + # for the k8s endpoint registry. We list instruction_following files + # individually instead of the whole resources_servers/instruction_following + # directory because that directory holds an 87 MB train.jsonl — Ray's + # 100 MiB working_dir cap would kick in and submission would fail. + # Listing app.py and requirements.txt by name picks up the FastAPI + # resource server and the rubric-grader wheels at daemon start + # (the `pip install -e "."` above reads them) without shipping the + # bulk dataset. The smaller validation.jsonl / example.jsonl / etc. + # are included because gym's smoke tests and rollout warm-up read + # them, and together they're under 2 MiB. + rayUploadPaths: + - nemo_rl + - infra/examples + - 3rdparty/Gym-workspace/Gym/nemo_gym + - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model + - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent + - 3rdparty/Gym-workspace/Gym/pyproject.toml + - 3rdparty/Gym-workspace/Gym/uv.lock + # app.py: the instruction_following FastAPI resource server entry. + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/app.py + # requirements.txt: rubric-grader deps gym installs on daemon start. + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/requirements.txt + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/tests + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/validation.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example_rollouts.jsonl + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {num-gpus: "0", dashboard-host: "0.0.0.0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-head + env: + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + resources: *shared_head_resources + ports: + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + - {containerPort: 9090, name: gym-server} + volumeMounts: *dshm_mounts + volumes: *dshm_4 + + # ------------------------------------------------------------------------- + # Training — GPU head + worker. Head has WANDB secret. + # ------------------------------------------------------------------------- + training: + name: raycluster-rl-qwen3-4b + labels: + disagg.nemo-rl/cluster: rl-qwen3-4b + disagg.nemo-rl/run: qwen3-4b-gym + # No daemon: waits for `nrl-k8s launch` / `nrl-k8s run`. + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-head + env: + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: GLOO_SOCKET_IFNAME, value: eth0} + - {name: NCCL_SOCKET_IFNAME, value: eth0} + - {name: NCCL_DEBUG, value: INFO} + - {name: NCCL_IB_DISABLE, value: "1"} + - {name: NCCL_NET, value: Socket} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: + name: wandb-api-key + key: WANDB_API_KEY + resources: *shared_head_resources + ports: *gpu_head_ports + volumeMounts: *dshm_mounts + volumes: *dshm_16 + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: {num-gpus: "8", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + # Same EFA + NCCL reasoning as the generation worker above. + # Training AllReduces run across these workers via aws-ofi; + # without hostNetwork they hang at the first collective. + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-worker + env: *shared_worker_env + resources: *gpu_worker_resources + volumeMounts: *dshm_mounts + volumes: *dshm_64 diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml new file mode 100644 index 00000000000..8c7d1b545f6 --- /dev/null +++ b/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml @@ -0,0 +1,33 @@ +# Recipe: Qwen3-4B disaggregated GRPO on instruction_following gym. +# +# Pure NeMo-RL recipe — no infra. Runs in any environment. Pair with an infra +# file via ``nrl-k8s run qwen3_4b_if.yaml --infra qwen3_4b_if.infra.yaml``. +defaults: ../../../examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml + +cluster: + gpus_per_node: 8 + num_nodes: 1 +grpo: + num_prompts_per_step: 32 + num_generations_per_prompt: 16 + max_num_steps: 200 + val_period: 50 + max_rollout_turns: 1 + async_grpo: + max_trajectory_age_steps: 1 +policy: + train_global_batch_size: 512 + train_micro_batch_size: 1 + logprob_batch_size: 1 + max_total_sequence_length: 16384 + dtensor_cfg: + activation_checkpointing: true +checkpointing: + save_period: 50 +logger: + wandb_enabled: true + wandb: + project: nemorl-full-disagg-k8s + name: full-disagg-if-qwen3-4b + tensorboard_enabled: true + monitor_gpus: true diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml new file mode 100644 index 00000000000..789e9d9ac7e --- /dev/null +++ b/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml @@ -0,0 +1,219 @@ +# Infra for gym-disaggregated Qwen3-4B colocated GRPO run. +# +# Two RayClusters: a single GPU cluster for training (with generation +# colocated inside it) and the usual CPU-only gym cluster. No separate +# generation cluster. + +_shared: + nodeSelector: &shared_node_selector + gpu-wrangler.nvidia.com/lease: nemo-rl-testing + tolerations: &shared_tolerations + - {key: gpu-wrangler.nvidia.com/lease, operator: Equal, value: nemo-rl-testing, effect: NoSchedule} + - {key: platform.nvidia.com/gpu, operator: Equal, value: "true", effect: NoSchedule} + # Image ships aws-ofi + enp71s0 defaults; keep pod env minimal. + headEnv: &shared_head_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + workerEnv: &shared_worker_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + headResources: &shared_head_resources + limits: {cpu: "8", memory: "32Gi"} + requests: {cpu: "2", memory: "8Gi"} + gpuWorkerResources: &gpu_worker_resources + # Sized for p5.48xlarge (192 CPU / ~1957 GiB allocatable). Claim most + # of the node; leave ~16 CPU + ~150 GiB for the training head, a + # colocated gym head, and k8s daemonsets (kube-proxy, aws-node, + # nvidia-device-plugin, node-exporter). + limits: + cpu: "176" + memory: "1800Gi" + nvidia.com/gpu: "8" + vpc.amazonaws.com/efa: "32" + requests: + cpu: "176" + memory: "1800Gi" + nvidia.com/gpu: "8" + vpc.amazonaws.com/efa: "32" + gpuHeadPorts: &gpu_head_ports + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + dshmMounts: &dshm_mounts + - {mountPath: /dev/shm, name: dshm} + dshm16: &dshm_16 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 16Gi} + dshm64: &dshm_64 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 64Gi} + dshm4: &dshm_4 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 4Gi} + +namespace: nemo-rl-testing +image: nvcr.io/nvidian/nemo-rl:nightly +imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] +serviceAccount: nemo-rl-endpoint-registry + +launch: + mode: attach + attach: + gym: raycluster-gym-disagg-gym-qwen3-4b + training: raycluster-gym-disagg-qwen3-4b + peerWatcher: false + # Colocated generation → no --remote_generation_url override. + # All env flows through inline exports (runtime_env.env_vars merge-fails + # when ray.init also has env_vars). + entrypoint: | + pip install kubernetes -q || true + export GLOO_SOCKET_IFNAME=enp71s0 + export NCCL_SOCKET_IFNAME=enp71s0 + export FI_PROVIDER=efa + # Data files are pre-staged on the pods via kubectl cp. + export DISAGG_TRAIN_PATH=/tmp/train.jsonl + export DISAGG_VALID_PATH=/tmp/validation.jsonl + # NOTE: PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True is incompatible + # with colocated vLLM's CuMemAllocator (AssertionError at vLLM init). + # Disagg sets it because gen doesn't use the memory pool; single does. + export RAY_enable_infeasible_task_early_exit=true + export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM + python -u examples/nemo_gym/run_grpo_nemo_gym.py \ + --config nrl_k8s_run.yaml \ + +env.disagg_job_id=qwen3-4b-gym-disagg \ + ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ + ~policy.optimizer.kwargs.foreach \ + ~policy.optimizer.kwargs.fused + env: {} + rayUploadPaths: + - nemo_rl + - examples + - infra/examples + - tests/check_metrics.py + - tests/json_dump_tb_logs.py + - 3rdparty/Gym-workspace/Gym/nemo_gym + - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model + - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent + - 3rdparty/Gym-workspace/Gym/pyproject.toml + - 3rdparty/Gym-workspace/Gym/uv.lock + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs + - 3rdparty/Megatron-LM-workspace/Megatron-LM/megatron + +clusters: + # ---------------- Gym (CPU, head-only) ---------------- + gym: + name: raycluster-gym-disagg-gym-qwen3-4b + daemon: + submissionId: qwen3-4b-gym-disagg-server-v5 + entrypoint: | + set -eu + ROOT_DIR=$(pwd) + GYM_DIR=\${ROOT_DIR}/3rdparty/Gym-workspace/Gym + export PYTHONPATH=\${ROOT_DIR}:\${GYM_DIR}:\${PYTHONPATH-} + cd \${GYM_DIR} + mkdir -p cache + RAY_VERSION=$(python -c "import ray; print(ray.__version__)") + echo "ray==\${RAY_VERSION}" > /tmp/ray-pin.txt + pip install -e "." --constraint /tmp/ray-pin.txt -q + pip install kubernetes -q + # In single-cluster mode training (run_grpo_nemo_gym.py) publishes + # vllm_base_urls to the endpoint registry once colocated vLLM spawns. + # Gym blocks on that key until training publishes — a two-way rendezvous. + exec python -m nemo_gym.standalone_server \ + --job-id qwen3-4b-gym-disagg \ + --port 9090 \ + --model-name Qwen/Qwen3-4B-Instruct-2507 \ + --config-yaml \${ROOT_DIR}/infra/examples/gym_standalone_config_instruction_following.yaml + rayUploadPaths: + - nemo_rl + - infra/examples + - 3rdparty/Gym-workspace/Gym/nemo_gym + - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model + - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent + - 3rdparty/Gym-workspace/Gym/pyproject.toml + - 3rdparty/Gym-workspace/Gym/uv.lock + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/app.py + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/requirements.txt + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/tests + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/validation.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example_rollouts.jsonl + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {num-gpus: "0", dashboard-host: "0.0.0.0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-head + env: + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + resources: *shared_head_resources + ports: + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + - {containerPort: 9090, name: gym-server} + volumeMounts: *dshm_mounts + volumes: *dshm_4 + + # ---------------- Training (GPU head + worker, generation colocated) ---------------- + training: + name: raycluster-gym-disagg-qwen3-4b + labels: + disagg.nemo-rl/cluster: gym-disagg-qwen3-4b + disagg.nemo-rl/run: qwen3-4b-gym-disagg + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-head + env: + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: + name: wandb-api-key + key: WANDB_API_KEY + resources: *shared_head_resources + ports: *gpu_head_ports + volumeMounts: *dshm_mounts + volumes: *dshm_16 + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: {num-gpus: "8", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-worker + env: *shared_worker_env + resources: *gpu_worker_resources + volumeMounts: *dshm_mounts + volumes: *dshm_64 diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml new file mode 100644 index 00000000000..ad15c927faa --- /dev/null +++ b/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml @@ -0,0 +1,49 @@ +# Qwen3-4B GRPO on instruction_following gym — **gym-disaggregated variant**. +# +# Differs from qwen3_4b_if.yaml in a single field: generation is colocated +# with training (both run inside the same Ray cluster's GPU workers), so we +# don't need a standalone generation RayCluster or its daemon. +# +# Pair with qwen3_4b_if_gym_disagg.infra.yaml (which declares only training + +# gym clusters — no generation cluster). +defaults: ../../../examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml + +cluster: + gpus_per_node: 8 + num_nodes: 1 +grpo: + num_prompts_per_step: 32 + num_generations_per_prompt: 16 + max_num_steps: 200 + val_period: 50 + max_rollout_turns: 1 + async_grpo: + # NeMo-RL asserts non-colocated when async_grpo is enabled. Single-cluster + # runs colocated generation, so we keep GRPO synchronous here. + enabled: false +policy: + train_global_batch_size: 256 + train_micro_batch_size: 1 + logprob_batch_size: 1 + # Colocated vLLM + backward on 4B takes too much GPU at 16K context; + # halving leaves headroom for vLLM's 6 GiB + activations + optimizer. + max_total_sequence_length: 8192 + dtensor_cfg: + activation_checkpointing: true + generation: + colocated: + enabled: true # recipe-level difference from disagg + resources: + gpus_per_node: 8 + num_nodes: 1 + vllm_cfg: + gpu_memory_utilization: 0.45 # leave 55% for training state +checkpointing: + save_period: 50 +logger: + wandb_enabled: true + wandb: + project: nemorl-gym-disagg-k8s + name: gym-disagg-if-qwen3-4b + tensorboard_enabled: true + monitor_gpus: true diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml new file mode 100644 index 00000000000..9f0ee75977d --- /dev/null +++ b/tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml @@ -0,0 +1,166 @@ +# Infra for single-cluster Qwen3-4B colocated GRPO run. +# +# A single RayCluster hosting training + colocated vLLM generation + a local +# Gym Ray actor. No separate gym or generation cluster — that's the whole +# point of single-cluster: the training entrypoint calls +# create_env(env_name="nemo_gym", ...) +# which pins the Gym actor to a non-head worker via NodeAffinityScheduling, +# so we must size the GPU worker big enough to host *both* the vLLM engines +# (8 GPU) and the Gym actor (~4 CPU). Namespace is auto-inferred from the +# kube context (nemo-rl-testing). + +_shared: + nodeSelector: &shared_node_selector + gpu-wrangler.nvidia.com/lease: nemo-rl-testing + tolerations: &shared_tolerations + - {key: gpu-wrangler.nvidia.com/lease, operator: Equal, value: nemo-rl-testing, effect: NoSchedule} + - {key: platform.nvidia.com/gpu, operator: Equal, value: "true", effect: NoSchedule} + # Image ships aws-ofi + enp71s0 defaults; keep pod env minimal. + headEnv: &shared_head_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + workerEnv: &shared_worker_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: GLOO_SOCKET_IFNAME, value: enp71s0} + - {name: NCCL_SOCKET_IFNAME, value: enp71s0} + - {name: FI_PROVIDER, value: efa} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + headResources: &shared_head_resources + limits: {cpu: "8", memory: "32Gi"} + requests: {cpu: "2", memory: "8Gi"} + gpuWorkerResources: &gpu_worker_resources + # Sized for the full p5.48xlarge (192 CPU / ~1957 GiB allocatable). + # Single-cluster needs the worker to host training + colocated vLLM + + # the Gym actor (~4 CPU). We claim 184 CPU / 1850 GiB (leaving ~8 CPU + + # ~100 GiB for the Ray head pod + k8s daemonsets: kube-proxy, aws-node, + # nvidia-device-plugin, node-exporter). 32 EFA devices = full NIC set. + limits: + cpu: "184" + memory: "1850Gi" + nvidia.com/gpu: "8" + vpc.amazonaws.com/efa: "32" + requests: + cpu: "184" + memory: "1850Gi" + nvidia.com/gpu: "8" + vpc.amazonaws.com/efa: "32" + gpuHeadPorts: &gpu_head_ports + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + dshmMounts: &dshm_mounts + - {mountPath: /dev/shm, name: dshm} + dshm16: &dshm_16 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 16Gi} + dshm64: &dshm_64 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 64Gi} + +# namespace intentionally omitted — CLI auto-infers nemo-rl-testing from +# the current kube context. +image: nvcr.io/nvidian/nemo-rl:nightly +imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] +serviceAccount: nemo-rl-endpoint-registry + +launch: + mode: attach + attach: + training: raycluster-single-qwen3-4b + peerWatcher: false + # Single-cluster: no remote_generation_url (colocated), no disagg_job_id + # (no endpoint registry — gym is a local Ray actor). + entrypoint: | + pip install kubernetes -q || true + export GLOO_SOCKET_IFNAME=enp71s0 + export NCCL_SOCKET_IFNAME=enp71s0 + export FI_PROVIDER=efa + # Data files are pre-staged on the pods via kubectl cp. + export DISAGG_TRAIN_PATH=/tmp/train.jsonl + export DISAGG_VALID_PATH=/tmp/validation.jsonl + # NOTE: PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True is incompatible + # with colocated vLLM's CuMemAllocator (AssertionError at vLLM init). + export RAY_enable_infeasible_task_early_exit=true + export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM + python -u examples/nemo_gym/run_grpo_nemo_gym.py \ + --config nrl_k8s_run.yaml \ + ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ + ~policy.optimizer.kwargs.foreach \ + ~policy.optimizer.kwargs.fused + env: {} + rayUploadPaths: + - nemo_rl + - examples + - infra/examples + - tests/check_metrics.py + - tests/json_dump_tb_logs.py + - 3rdparty/Gym-workspace/Gym/nemo_gym + - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model + - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent + - 3rdparty/Gym-workspace/Gym/pyproject.toml + - 3rdparty/Gym-workspace/Gym/uv.lock + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/app.py + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/requirements.txt + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/tests + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/validation.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example_rollouts.jsonl + - 3rdparty/Megatron-LM-workspace/Megatron-LM/megatron + +clusters: + # ---------------- Training (GPU head + worker: generation + gym colocated) ---------------- + training: + name: raycluster-single-qwen3-4b + labels: + disagg.nemo-rl/cluster: single-qwen3-4b + disagg.nemo-rl/run: qwen3-4b-single + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-head + env: + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: + name: wandb-api-key + key: WANDB_API_KEY + resources: *shared_head_resources + ports: *gpu_head_ports + volumeMounts: *dshm_mounts + volumes: *dshm_16 + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: {num-gpus: "8", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-worker + env: *shared_worker_env + resources: *gpu_worker_resources + volumeMounts: *dshm_mounts + volumes: *dshm_64 diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_single.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_single.yaml new file mode 100644 index 00000000000..fea7d828533 --- /dev/null +++ b/tools/nrl_k8s/examples/qwen3_4b_if_single.yaml @@ -0,0 +1,50 @@ +# Qwen3-4B GRPO on instruction_following gym — **single RayCluster variant**. +# +# Training, colocated vLLM generation, AND gym all run inside a single +# RayCluster. Gym is spawned as a local Ray actor by +# examples/nemo_gym/run_grpo_nemo_gym.py (create_env(env_name="nemo_gym", ...)) +# because neither env.disagg_job_id nor env.remote_gym_url is set. +# +# Pair with qwen3_4b_if_single.infra.yaml (declares only the training cluster; +# no separate gym or generation cluster). +defaults: ../../../examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml + +cluster: + gpus_per_node: 8 + num_nodes: 1 +grpo: + num_prompts_per_step: 32 + num_generations_per_prompt: 16 + max_num_steps: 200 + val_period: 50 + max_rollout_turns: 1 + async_grpo: + # NeMo-RL asserts non-colocated when async_grpo is enabled. Single-cluster runs + # colocated generation, so GRPO must remain synchronous. + enabled: false +policy: + train_global_batch_size: 256 + train_micro_batch_size: 1 + logprob_batch_size: 1 + # Colocated vLLM + backward on 4B takes too much GPU at 16K context; + # halving leaves headroom for vLLM's KV cache + activations + optimizer. + max_total_sequence_length: 8192 + dtensor_cfg: + activation_checkpointing: true + generation: + colocated: + enabled: true + resources: + gpus_per_node: 8 + num_nodes: 1 + vllm_cfg: + gpu_memory_utilization: 0.45 # leave 55% for training state +checkpointing: + save_period: 50 +logger: + wandb_enabled: true + wandb: + project: nemorl-single-k8s + name: single-if-qwen3-4b + tensorboard_enabled: true + monitor_gpus: true diff --git a/tools/nrl_k8s/pyproject.toml b/tools/nrl_k8s/pyproject.toml new file mode 100644 index 00000000000..fa8a67be3c9 --- /dev/null +++ b/tools/nrl_k8s/pyproject.toml @@ -0,0 +1,41 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "nrl-k8s" +version = "0.1.0" +description = "Config-driven launcher that runs NeMo-RL recipes on Kubernetes." +readme = "README.md" +requires-python = ">=3.10" +license = { text = "Apache-2.0" } +authors = [{ name = "NVIDIA NeMo-RL" }] +dependencies = [ + "click>=8.1", + "pydantic>=2.5", + "omegaconf>=2.3", + "pyyaml>=6.0", + "kubernetes>=29.0", + "ray[default]>=2.52", + "tenacity>=8.2", +] + +[project.optional-dependencies] +test = ["pytest>=7.4", "pytest-mock>=3.12"] +all = ["nrl-k8s[test]"] + +[project.scripts] +nrl-k8s = "nrl_k8s.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] +include = ["nrl_k8s*"] + +[tool.setuptools.package-data] +nrl_k8s = ["defaults/*.yaml"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +markers = [ + "kind: integration tests that require a kind cluster with KubeRay installed", +] diff --git a/tools/nrl_k8s/src/nrl_k8s/__init__.py b/tools/nrl_k8s/src/nrl_k8s/__init__.py new file mode 100644 index 00000000000..2571088acc0 --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/__init__.py @@ -0,0 +1,3 @@ +"""nrl-k8s: config-driven launcher for NeMo-RL recipes on Kubernetes.""" + +__version__ = "0.1.0" diff --git a/tools/nrl_k8s/src/nrl_k8s/_logging.py b/tools/nrl_k8s/src/nrl_k8s/_logging.py new file mode 100644 index 00000000000..6a94e99492a --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/_logging.py @@ -0,0 +1,64 @@ +"""Redaction helpers used before logging k8s objects. + +Manifests carry secret material in two common shapes: ConfigMap/Secret +``data`` blocks and container ``env`` entries whose name matches a +well-known credential pattern. Surface-level log lines should never show +either, even on error paths where we dump the full body for context. +""" + +from __future__ import annotations + +import copy +import re +from typing import Any + +_SECRET_NAME_RE = re.compile(r"(TOKEN|KEY|PASSWORD|SECRET|CRED|PASSWD)", re.IGNORECASE) +_REDACTED = "***REDACTED***" + + +def _redact_env_list(envs: list[Any]) -> None: + for entry in envs: + if not isinstance(entry, dict): + continue + name = entry.get("name", "") or "" + is_secretish = bool(_SECRET_NAME_RE.search(name)) + if "valueFrom" in entry and isinstance(entry["valueFrom"], dict): + vf = entry["valueFrom"] + if "secretKeyRef" in vf: + entry["valueFrom"] = {"secretKeyRef": _REDACTED} + if is_secretish and "value" in entry: + entry["value"] = _REDACTED + + +def redact(obj: Any) -> Any: + """Return a deep copy of ``obj`` with secret-looking fields masked. + + Handles ConfigMap/Secret ``data`` + ``stringData`` blocks and + ``env[].value`` / ``env[].valueFrom.secretKeyRef`` across every + pod template (head + worker groups). + """ + if not isinstance(obj, (dict, list)): + return obj + + scrubbed = copy.deepcopy(obj) + _walk(scrubbed) + return scrubbed + + +def _walk(node: Any) -> None: + if isinstance(node, dict): + kind = node.get("kind") + if kind in ("ConfigMap", "Secret"): + for key in ("data", "stringData"): + if isinstance(node.get(key), dict): + node[key] = {k: _REDACTED for k in node[key]} + if "env" in node and isinstance(node["env"], list): + _redact_env_list(node["env"]) + for v in node.values(): + _walk(v) + elif isinstance(node, list): + for item in node: + _walk(item) + + +__all__ = ["redact"] diff --git a/tools/nrl_k8s/src/nrl_k8s/_retry.py b/tools/nrl_k8s/src/nrl_k8s/_retry.py new file mode 100644 index 00000000000..2364b4b819f --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/_retry.py @@ -0,0 +1,78 @@ +"""Retry wrapper for transient Kubernetes API failures. + +5xx responses and connection resets are common on busy clusters. Our +apply/get/list calls are side-effect-free or idempotent, so a short +exponential backoff keeps the CLI usable without masking real bugs. + +4xx (bad manifest, missing RBAC) is not retried — those are our bugs. +""" + +from __future__ import annotations + +import socket +from functools import wraps +from typing import Callable, TypeVar + +from kubernetes.client.exceptions import ApiException +from tenacity import ( + Retrying, + retry_if_exception, + stop_after_attempt, + wait_exponential, +) +from urllib3.exceptions import ProtocolError, ReadTimeoutError + +T = TypeVar("T") + +_RETRIABLE_STATUSES = frozenset({500, 502, 503, 504}) +_RETRIABLE_NETWORK = ( + ReadTimeoutError, + ProtocolError, + socket.timeout, + ConnectionResetError, + OSError, +) + + +def _is_transient(exc: BaseException) -> bool: + if isinstance(exc, ApiException): + return exc.status in _RETRIABLE_STATUSES + return isinstance(exc, _RETRIABLE_NETWORK) + + +def with_retries( + func: Callable[[], T], + *, + retries: int = 3, + max_wait: float = 2.0, +) -> T: + """Call ``func`` up to ``retries`` times on transient k8s API errors. + + Backs off exponentially (0.5s, 1s, capped at ``max_wait``) between + attempts. Re-raises the last exception once attempts are exhausted. + """ + retrying = Retrying( + retry=retry_if_exception(_is_transient), + stop=stop_after_attempt(retries), + wait=wait_exponential(multiplier=1, min=0.5, max=max_wait), + reraise=True, + ) + return retrying(func) + + +def retry_transient(retries: int = 3, max_wait: float = 2.0): + """Decorator flavour of :func:`with_retries` for module-level helpers.""" + + def wrap(func: Callable[..., T]) -> Callable[..., T]: + @wraps(func) + def inner(*args, **kwargs): + return with_retries( + lambda: func(*args, **kwargs), retries=retries, max_wait=max_wait + ) + + return inner + + return wrap + + +__all__ = ["retry_transient", "with_retries"] diff --git a/tools/nrl_k8s/src/nrl_k8s/cli.py b/tools/nrl_k8s/src/nrl_k8s/cli.py new file mode 100644 index 00000000000..0b7664403b8 --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/cli.py @@ -0,0 +1,818 @@ +"""``nrl-k8s`` command-line entry point. + +Hydra-style overrides (``infra.scheduler.queue=x``) are collected via +``click.UNPROCESSED`` — any ``key=value`` token after the recipe path is +passed to :func:`nrl_k8s.config.load_recipe_with_infra` as an override. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path +from typing import NoReturn + +import click +import yaml +from kubernetes.client.exceptions import ApiException +from omegaconf import OmegaConf + +from . import __version__ +from .config import LoadedConfig, load_recipe_with_infra +from .orchestrate import ALL_ROLES +from .schema import ClusterSpec + +_INFRA_OPTION = click.option( + "--infra", + "infra_path", + type=click.Path(exists=True, dir_okay=False, path_type=Path), + default=None, + help="Path to a standalone infra YAML. When set, the recipe must not " + "contain an `infra:` key.", +) +_ROLE_CHOICE = click.Choice(list(ALL_ROLES)) + + +# ============================================================================= +# Root group +# ============================================================================= + + +@click.group(context_settings={"help_option_names": ["-h", "--help"]}) +@click.version_option(__version__, prog_name="nrl-k8s") +def main() -> None: + """Launch NeMo-RL recipes on Kubernetes.""" + + +# ============================================================================= +# check — load + validate + (optionally) render manifests +# ============================================================================= + + +@main.command() +@click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) +@_INFRA_OPTION +@click.option( + "--output", + "-o", + "output_path", + type=click.Path(dir_okay=False, path_type=Path), + default=None, + help="Write the full resolved config + rendered RayCluster manifests to " + "this file (yaml or json — extension picks the format). Omit to print " + "only a one-page summary.", +) +@click.option( + "--format", + "output_format", + type=click.Choice(["yaml", "json"]), + default=None, + help="Override the format when using --output. Defaults to the extension.", +) +def check( + recipe: Path, + overrides: tuple[str, ...], + infra_path: Path | None, + output_path: Path | None, + output_format: str | None, +) -> None: + """Load + validate a recipe/infra pair and print a one-line summary per + role (or dump the fully-resolved config + rendered RayCluster manifests + to a file with ``-o``). Replaces the former ``validate`` + ``plan``. + """ + from .manifest import build_raycluster_manifest + + try: + loaded = load_recipe_with_infra( + recipe, overrides=list(overrides), infra_path=infra_path + ) + except Exception as exc: # noqa: BLE001 — surface the full message to the user + _explain_and_exit(exc, context="failed to load recipe") + + manifests: dict[str, dict] = {} + for role in ALL_ROLES: + cluster = getattr(loaded.infra.clusters, role) + if cluster is None: + continue + manifests[role] = build_raycluster_manifest(cluster, loaded.infra) + + if output_path is not None: + _dump_check_output(loaded, manifests, output_path, output_format) + click.echo(f"wrote full config + {len(manifests)} manifest(s) to {output_path}") + return + + _print_check_summary(loaded, manifests) + + +def _print_check_summary(loaded: LoadedConfig, manifests: dict[str, dict]) -> None: + """One-page overview — namespace, image, launch/attach, per-role highlights.""" + infra = loaded.infra + click.echo(f"namespace: {infra.namespace}") + click.echo(f"image: {infra.image}") + if infra.imagePullSecrets: + click.echo(f"pullSecrets: {', '.join(infra.imagePullSecrets)}") + if infra.serviceAccount: + click.echo(f"sa: {infra.serviceAccount}") + click.echo( + f"scheduler: {infra.scheduler.kind.value}" + + (f" (queue={infra.scheduler.queue})" if infra.scheduler.queue else "") + ) + click.echo(f"launch.mode: {infra.launch.mode.value}") + if infra.launch.entrypoint: + click.echo("entrypoint:") + _print_block(infra.launch.entrypoint) + + click.echo("") + click.echo("CLUSTERS") + click.echo("--------") + if not manifests: + click.echo(" (none declared)") + return + for role, m in manifests.items(): + spec = m["spec"] + name = m["metadata"]["name"] + head = spec.get("headGroupSpec", {}).get("template", {}).get("spec", {}) + head_res = ( + head.get("containers", [{}])[0].get("resources", {}).get("limits") or {} + ) + workers = spec.get("workerGroupSpecs") or [] + wrep = sum(int(w.get("replicas", 0)) for w in workers) + wgpu = 0 + wcpu = wmem = "—" + if workers: + w_res = ( + workers[0] + .get("template", {}) + .get("spec", {}) + .get("containers", [{}])[0] + .get("resources", {}) + .get("limits") + or {} + ) + wgpu = int(w_res.get("nvidia.com/gpu", 0)) * wrep + wcpu = w_res.get("cpu", "—") + wmem = w_res.get("memory", "—") + daemon = loaded.infra.clusters.__dict__[role].daemon + daemon_id = daemon.submissionId if daemon else "—" + + click.echo(f" {role}: {name}") + click.echo( + f" head cpu={head_res.get('cpu', '—')} mem={head_res.get('memory', '—')}" + ) + if workers: + click.echo(f" workers {wrep}x cpu={wcpu} mem={wmem} gpu={wgpu}") + else: + click.echo(" workers (none — head-only)") + click.echo(f" daemon {daemon_id}") + if daemon and daemon.entrypoint: + click.echo(" entrypoint:") + _print_block(daemon.entrypoint, indent=" ") + + +def _print_block(text: str, *, indent: str = " ") -> None: + """Print a multi-line shell/script body with consistent indent.""" + # Trim surrounding blank lines but keep internal formatting. + lines = text.rstrip("\n").splitlines() + while lines and not lines[0].strip(): + lines.pop(0) + while lines and not lines[-1].strip(): + lines.pop() + for line in lines: + click.echo(f"{indent}{line}") + + +def _dump_check_output( + loaded: LoadedConfig, + manifests: dict[str, dict], + path: Path, + fmt_override: str | None, +) -> None: + fmt = fmt_override or ("json" if path.suffix == ".json" else "yaml") + bundle = { + "infra": loaded.infra.model_dump(mode="json"), + "recipe": OmegaConf.to_container(loaded.recipe, resolve=True), + "manifests": manifests, + } + path.parent.mkdir(parents=True, exist_ok=True) + if fmt == "json": + path.write_text(json.dumps(bundle, indent=2, sort_keys=True)) + else: + path.write_text(yaml.safe_dump(bundle, sort_keys=False)) + + +# ============================================================================= +# Deprecated aliases — kept only where scripts/docs still reference them. +# Unimplemented stub commands (doctor/dashboard/dev) were removed; see +# tools/nrl_k8s/docs/roadmap.md for the planned work. +# ============================================================================= + + +@main.command(hidden=True) +@click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) +@_INFRA_OPTION +@click.pass_context +def validate(ctx, recipe, overrides, infra_path) -> None: + """Deprecated: use ``check``. Prints the summary for backwards compat.""" + click.echo("note: `validate` is deprecated — use `check`.", err=True) + ctx.invoke( + check, + recipe=recipe, + overrides=overrides, + infra_path=infra_path, + output_path=None, + output_format=None, + ) + + +@main.command() +@click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) +@_INFRA_OPTION +@click.option( + "--repo-root", + type=click.Path(exists=True, file_okay=False, path_type=Path), + default=Path.cwd(), + show_default="cwd", + help="NeMo-RL repo root used to source files for the working_dir upload.", +) +@click.option( + "--follow", + is_flag=True, + help="Stream training-job logs after submit.", +) +@click.option( + "--replace", + is_flag=True, + help="Stop any running training job on the cluster before submitting.", +) +def launch( + recipe: Path, + overrides: tuple[str, ...], + infra_path: Path | None, + repo_root: Path, + follow: bool, + replace: bool, +) -> None: + """Submit a training job against an already-up training cluster. + + ``--replace`` stops any RUNNING Ray Job on the training cluster first so + the new submission doesn't queue behind GPU-holding stragglers. + """ + from . import orchestrate + from . import submit as submit_mod + + loaded = _load_or_exit(recipe, overrides, infra_path) + if not submit_mod.is_in_cluster(): + _preflight_or_exit(loaded.infra.namespace) + if not loaded.infra.launch.entrypoint: + _cli_error( + "infra.launch.entrypoint is empty", + hint="launch command requires infra.launch.entrypoint; see docs/recipes.md", + ) + try: + result = orchestrate.submit_training( + loaded, log=click.echo, repo_root=repo_root.resolve(), replace=replace + ) + except Exception as exc: # noqa: BLE001 + _explain_and_exit(exc, context="launch failed") + + click.echo(f"training job: {result.training_job_id}") + click.echo(f"dashboard: {result.training_dashboard}") + if follow: + _tail(result.training_dashboard, result.training_job_id) + + +@main.command() +@click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) +@_INFRA_OPTION +@click.option( + "--repo-root", + type=click.Path(exists=True, file_okay=False, path_type=Path), + default=Path.cwd(), + show_default="cwd", + help="NeMo-RL repo root used to source files for the working_dir upload.", +) +@click.option( + "--follow", + is_flag=True, + help="Stream training-job logs after submit.", +) +@click.option( + "--replace", + is_flag=True, + help="Stop any running daemon/training job before submitting new ones.", +) +def run( + recipe: Path, + overrides: tuple[str, ...], + infra_path: Path | None, + repo_root: Path, + follow: bool, + replace: bool, +) -> None: + """Bring up every cluster + daemon declared in the recipe, then submit training. + + One command takes a recipe from zero to a running job: apply each + RayCluster, submit its daemon (for generation/gym), then submit the + training entrypoint against the training cluster. + + ``--replace`` stops any previous RUNNING instance of a daemon or training + job before submitting (and suffixes daemon submissionIds with a + timestamp so Ray accepts the resubmit). Use it to re-run with code + changes without manually bumping IDs or calling ``job stop``. + """ + from . import orchestrate + from . import submit as submit_mod + + loaded = _load_or_exit(recipe, overrides, infra_path) + if not submit_mod.is_in_cluster(): + _preflight_or_exit(loaded.infra.namespace) + try: + result = orchestrate.run( + loaded, log=click.echo, repo_root=repo_root.resolve(), replace=replace + ) + except Exception as exc: # noqa: BLE001 + _explain_and_exit(exc, context="run failed") + + click.echo(f"training job: {result.training_job_id}") + click.echo(f"dashboard: {result.training_dashboard}") + if follow: + _tail(result.training_dashboard, result.training_job_id) + + +@main.command() +@click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) +@_INFRA_OPTION +def status(recipe: Path, overrides: tuple[str, ...], infra_path: Path | None) -> None: + """Summarise every cluster declared in the recipe. + + Prints, per role: RayCluster state, head pod phase, worker pod phases, + and (if a daemon is declared) its Ray Job status. + """ + from . import inspect as ins + + loaded = _load_or_exit(recipe, overrides, infra_path) + rows = ins.collect_status(loaded) + if not rows: + click.echo("(no clusters declared in recipe)") + return + + header = ( + f"{'ROLE':<11} {'NAME':<36} {'STATE':<9} {'HEAD':<9} {'WORKERS':<20} DAEMON" + ) + click.echo(header) + click.echo("-" * len(header)) + for row in rows: + workers = ",".join(row.worker_phases) or "—" + daemon = ( + f"{row.daemon_submission_id}={row.daemon_status or 'unknown'}" + if row.daemon_submission_id + else "—" + ) + click.echo( + f"{row.role:<11} {row.name:<36} {row.state:<9} " + f"{(row.head_phase or '—'):<9} {workers:<20} {daemon}" + ) + + +@main.command() +@click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) +@_INFRA_OPTION +@click.option( + "--role", + type=_ROLE_CHOICE, + required=True, + help="Which cluster's logs to tail.", +) +@click.option( + "--source", + type=click.Choice(["auto", "daemon", "head", "worker"]), + default="auto", + show_default=True, + help="'auto' = daemon Ray Job if the role has one, else head pod.", +) +@click.option("-f", "--follow", is_flag=True, help="Stream new output until Ctrl+C.") +@click.option( + "--tail", + "tail_lines", + type=int, + default=200, + show_default=True, + help="Number of trailing lines to show before following.", +) +def logs( + recipe: Path, + overrides: tuple[str, ...], + infra_path: Path | None, + role: str, + source: str, + follow: bool, + tail_lines: int, +) -> None: + """Stream logs from a role's cluster. + + When the role has a daemon (generation / gym), ``--source auto`` shows + the daemon's Ray Job logs via the dashboard. Otherwise it falls back + to the head pod's container logs via kubectl. + """ + from . import inspect as ins + + loaded = _load_or_exit(recipe, overrides, infra_path) + cluster = _pick_cluster_or_exit(loaded, role) + namespace = loaded.infra.namespace + + effective = source + if effective == "auto": + effective = "daemon" if cluster.daemon is not None else "head" + + if effective == "daemon": + if cluster.daemon is None or not cluster.daemon.submissionId: + _cli_error( + f"role {role} has no daemon submissionId", + hint=f"use --source head|worker, or declare `clusters.{role}.daemon.submissionId`", + ) + _tail_daemon(cluster.name, namespace, cluster.daemon.submissionId) + return + + # Pod logs — head or a worker. + if effective == "head": + pod_name = ins.head_pod_name(cluster.name, namespace) + else: + pod_name = _first_worker_pod_or_exit(cluster.name, namespace) + + for line in ins.stream_pod_logs( + pod_name, namespace, follow=follow, tail_lines=tail_lines + ): + click.echo(line, nl=False) + + +# ---- `cluster` group ---------------------------------------------------- + + +@main.group() +def cluster() -> None: + """Manage long-lived RayClusters (generation, gym, training).""" + + +@cluster.command("up") +@click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) +@_INFRA_OPTION +@click.option( + "--role", + type=_ROLE_CHOICE, + required=True, +) +@click.option( + "--wait/--no-wait", + default=True, + help="Wait for the cluster to reach state=ready before returning.", +) +@click.option( + "--timeout", + default=900, + show_default=True, + help="Seconds to wait for readiness when --wait is set.", +) +@click.option( + "--dry-run", + is_flag=True, + help="Render the RayCluster manifest for the role and print it; do not apply.", +) +def cluster_up( + recipe: Path, + overrides: tuple[str, ...], + infra_path: Path | None, + role: str, + wait: bool, + timeout: int, + dry_run: bool, +) -> None: + """Bring up a RayCluster, then submit its daemon if the recipe has one.""" + from . import orchestrate + from .manifest import build_raycluster_manifest + + loaded = _load_or_exit(recipe, overrides, infra_path) + cluster_spec = _pick_cluster_or_exit(loaded, role) + if dry_run: + manifest = build_raycluster_manifest(cluster_spec, loaded.infra) + click.echo(yaml.safe_dump(manifest, sort_keys=False).rstrip()) + return + + try: + name = orchestrate.bring_up_cluster( + role, loaded, log=click.echo, wait_ready=wait, ready_timeout_s=timeout + ) + if wait: + # Only submit the daemon once the cluster is ready (matches + # the `run` flow — same code path). + orchestrate.submit_daemon( + role, + loaded, + name, + log=click.echo, + repo_root=Path.cwd(), + ) + except ApiException as exc: + if exc.status == 403: + _cli_error( + f"forbidden to create RayCluster in {loaded.infra.namespace}", + hint="missing RBAC — run `nrl-k8s doctor` or ask an admin to grant the edit role.", + ) + _explain_and_exit(exc, context=f"cluster up ({role}) failed") + except Exception as exc: # noqa: BLE001 + _explain_and_exit(exc, context=f"cluster up ({role}) failed") + + +@cluster.command("down") +@click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) +@_INFRA_OPTION +@click.option( + "--role", + type=_ROLE_CHOICE, + help="Delete the cluster for this role (uses recipe to resolve name).", +) +@click.option( + "--name", + "name_opt", + help="Delete a RayCluster by name directly (overrides --role).", +) +@click.option( + "--wait/--no-wait", + default=True, + help="Wait for the RayCluster object to disappear.", +) +def cluster_down( + recipe: Path, + overrides: tuple[str, ...], + infra_path: Path | None, + role: str | None, + name_opt: str | None, + wait: bool, +) -> None: + """Delete a managed RayCluster by role or by name.""" + from . import k8s + + loaded = _load_or_exit(recipe, overrides, infra_path) + namespace = loaded.infra.namespace + + if name_opt: + target = name_opt + elif role: + cluster = _pick_cluster_or_exit(loaded, role) + target = cluster.name + else: + _cli_error( + "pass --role or --name", + hint="e.g. `nrl-k8s cluster down recipe.yaml --role training`", + exit_code=2, + ) + + click.echo(f"deleting RayCluster {target} in {namespace} ...") + k8s.delete_raycluster(target, namespace) + if wait: + k8s.wait_for_raycluster_gone(target, namespace) + click.echo(f"RayCluster {target} deleted.") + + +@cluster.command("list") +@click.option( + "--namespace", + "-n", + default=None, + help="Kubernetes namespace to list. Defaults to the current kube context's namespace.", +) +def cluster_list(namespace: str | None) -> None: + """List RayClusters in a namespace and their state.""" + from . import k8s + from .config import _infer_kube_namespace + + ns = namespace or _infer_kube_namespace() + rows = k8s.list_rayclusters(ns) + if not rows: + click.echo(f"(no RayClusters in {ns})") + return + for obj in rows: + name = obj["metadata"]["name"] + state = obj.get("status", {}).get("state", "—") + click.echo(f"{name}\t{state}") + + +# ---- `job` group -------------------------------------------------------- + + +@main.group() +def job() -> None: + """Inspect and control Ray jobs on managed clusters.""" + + +@job.command("list") +@click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) +@_INFRA_OPTION +@click.option( + "--role", + type=_ROLE_CHOICE, + required=True, + help="Which cluster's Ray jobs to list.", +) +def job_list( + recipe: Path, + overrides: tuple[str, ...], + infra_path: Path | None, + role: str, +) -> None: + """List Ray Jobs currently registered on a role's RayCluster.""" + from ray.job_submission import JobSubmissionClient + + from . import submit + + loaded = _load_or_exit(recipe, overrides, infra_path) + cluster = _pick_cluster_or_exit(loaded, role) + namespace = loaded.infra.namespace + + try: + with submit.dashboard_url(cluster.name, namespace) as dash: + clnt = JobSubmissionClient(dash) + jobs = clnt.list_jobs() + except Exception as exc: # noqa: BLE001 + _explain_and_exit(exc, context="list jobs failed") + + if not jobs: + click.echo(f"(no Ray jobs on {cluster.name})") + return + click.echo(f"{'SUBMISSION':<40} {'STATUS':<12} ENTRYPOINT") + for j in jobs: + entry = (j.entrypoint or "").splitlines()[0][:80] + click.echo(f"{j.submission_id:<40} {j.status.value:<12} {entry}") + + +@job.command("logs") +@click.argument("submission_id") +@click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) +@_INFRA_OPTION +@click.option( + "--role", + type=_ROLE_CHOICE, + required=True, + help="Which cluster hosts the job.", +) +def job_logs( + submission_id: str, + recipe: Path, + overrides: tuple[str, ...], + infra_path: Path | None, + role: str, +) -> None: + """Stream logs for a Ray Job by submission id on a given role's cluster.""" + loaded = _load_or_exit(recipe, overrides, infra_path) + cluster = _pick_cluster_or_exit(loaded, role) + _tail_daemon(cluster.name, loaded.infra.namespace, submission_id) + + +@job.command("stop") +@click.argument("submission_id") +@click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) +@_INFRA_OPTION +@click.option( + "--role", + type=_ROLE_CHOICE, + required=True, + help="Which cluster hosts the job.", +) +def job_stop( + submission_id: str, + recipe: Path, + overrides: tuple[str, ...], + infra_path: Path | None, + role: str, +) -> None: + """Stop a Ray Job by submission id.""" + from ray.job_submission import JobSubmissionClient + + from . import submit + + loaded = _load_or_exit(recipe, overrides, infra_path) + cluster = _pick_cluster_or_exit(loaded, role) + try: + with submit.dashboard_url(cluster.name, loaded.infra.namespace) as dash: + clnt = JobSubmissionClient(dash) + clnt.stop_job(submission_id) + except Exception as exc: # noqa: BLE001 + _explain_and_exit(exc, context=f"stop {submission_id} failed") + click.echo(f"stopped {submission_id}") + + +# ============================================================================= +# Helpers +# ============================================================================= + + +def _preflight_or_exit(namespace: str) -> None: + """Fail fast when kubectl is missing or RBAC is wrong — before we spawn anything.""" + from . import submit + + try: + submit.kubectl_preflight(namespace) + except RuntimeError as exc: + _cli_error(str(exc), hint="see `nrl-k8s doctor` for cluster access checks") + + +def _cli_error(msg: str, *, hint: str | None = None, exit_code: int = 1) -> NoReturn: + """Emit a stderr error with an optional actionable hint, then exit.""" + click.echo(f"error: {msg}", err=True) + if hint: + click.echo(f"hint: {hint}", err=True) + sys.exit(exit_code) + + +def _explain_and_exit(exc: BaseException, *, context: str) -> NoReturn: + """Map common exceptions to an actionable hint before exiting.""" + hint: str | None = None + if isinstance(exc, ApiException): + if exc.status == 403: + hint = ( + "missing RBAC for this action; run `nrl-k8s doctor` or ask an " + "admin to grant the edit role on the namespace." + ) + elif exc.status == 401: + hint = "kubectl credentials rejected; try `aws sso login`." + elif exc.status in (500, 502, 503, 504): + hint = "control-plane 5xx — retry in a few seconds." + elif isinstance(exc, ConnectionRefusedError): + hint = ( + "connection refused — kubectl port-forward to the dashboard failed; " + "is kubectl authenticated? (try `aws sso login`)" + ) + elif isinstance(exc, ValueError) and "launch.entrypoint" in str(exc): + hint = "set infra.launch.entrypoint in your recipe; see docs/recipes.md." + _cli_error(f"{context}: {exc}", hint=hint) + + +def _load_or_exit( + recipe: Path, overrides: tuple[str, ...], infra_path: Path | None = None +) -> LoadedConfig: + try: + return load_recipe_with_infra( + recipe, overrides=list(overrides), infra_path=infra_path + ) + except Exception as exc: # noqa: BLE001 + _explain_and_exit(exc, context="failed to load recipe") + + +def _pick_cluster_or_exit(loaded: LoadedConfig, role: str) -> ClusterSpec: + cluster = getattr(loaded.infra.clusters, role) + if cluster is None: + _cli_error( + f"infra.clusters.{role} is not defined in {loaded.source_path}", + hint=f"declare a `clusters.{role}` block in the recipe or pass a different --role", + ) + return cluster + + +def _tail(dashboard: str, job_id: str) -> None: + """Stream Ray Job logs to stdout until terminal or Ctrl+C.""" + from . import submit as submit_mod + + try: + for line in submit_mod.tail_job_logs(dashboard, job_id): + click.echo(line, nl=False) + except KeyboardInterrupt: + click.echo("\n(interrupted — job continues running)", err=True) + + +def _tail_daemon(cluster_name: str, namespace: str, submission_id: str) -> None: + """Open a dashboard port-forward and tail a Ray Job by submission_id.""" + from . import submit as submit_mod + + try: + with submit_mod.dashboard_url(cluster_name, namespace) as dash: + click.echo(f"# tailing {submission_id} via {dash}", err=True) + _tail(dash, submission_id) + except KeyboardInterrupt: + click.echo("\n(interrupted — job continues running)", err=True) + except Exception as exc: # noqa: BLE001 + _explain_and_exit(exc, context=f"tailing {submission_id} failed") + + +def _first_worker_pod_or_exit(cluster_name: str, namespace: str) -> str: + from . import inspect as ins + + pods = ins.list_cluster_pods(cluster_name, namespace) + if not pods.worker_names: + _cli_error( + f"no worker pods for {cluster_name} in {namespace}", + hint="is the RayCluster still scheduling? check `nrl-k8s status` first.", + ) + return pods.worker_names[0] + + +__all__ = ["main"] diff --git a/tools/nrl_k8s/src/nrl_k8s/config.py b/tools/nrl_k8s/src/nrl_k8s/config.py new file mode 100644 index 00000000000..93e78b6d97a --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/config.py @@ -0,0 +1,296 @@ +"""Recipe + infra config loader for ``nrl-k8s``. + +The CLI's single source of truth for a run. Given a recipe path, loads and +merges four layers in priority order (low to high): + + 1. Shipped defaults — ``nrl_k8s/defaults/defaults.example.yaml`` + 2. User defaults — ``~/.config/nrl-k8s/defaults.yaml`` (optional) + 3. Recipe-level ``infra:`` — the ``infra:`` key on the recipe YAML, if present + 4. CLI Hydra overrides — ``infra.scheduler.queue=my-queue``, etc. + +Layers 1-3 are YAML; layer 4 is a list of Hydra-style strings from the CLI. The +merged ``infra`` mapping is validated through :class:`nrl_k8s.schema.InfraConfig`. + +The rest of the recipe (``policy``, ``grpo``, ``data``, ``logger``, ...) is +loaded via NeMo-RL's own loader when available, so defaults/inheritance +(``defaults: ../../grpo_math_1B.yaml``) and ``${mul:...}`` resolvers are +handled exactly the same way as the existing training entrypoints. + +If ``nemo_rl`` is not importable (e.g. the CLI is installed standalone on a +dev machine without the full repo), a minimal OmegaConf fallback handles a +single-file recipe. Inheritance is not supported in fallback mode. +""" + +from __future__ import annotations + +import os +from dataclasses import dataclass +from pathlib import Path + +from omegaconf import DictConfig, OmegaConf + +from .schema import InfraConfig + +_SHIPPED_DEFAULTS = Path(__file__).parent / "defaults" / "defaults.example.yaml" +_USER_DEFAULTS = Path( + os.environ.get( + "NRL_K8S_DEFAULTS", Path.home() / ".config" / "nrl-k8s" / "defaults.yaml" + ) +) + + +@dataclass +class LoadedConfig: + """Bundle returned by :func:`load_recipe_with_infra`. + + ``recipe`` holds the resolved recipe with ``infra`` removed (so it can be + passed to the NeMo-RL entry-point as-is). ``infra`` is the validated + :class:`InfraConfig` instance. ``source_path`` is the recipe path we loaded. + """ + + recipe: DictConfig + infra: InfraConfig + source_path: Path + + +# ============================================================================= +# Public API +# ============================================================================= + + +def load_recipe_with_infra( + recipe_path: str | Path, + overrides: list[str] | None = None, + *, + infra_path: str | Path | None = None, +) -> LoadedConfig: + """Load a NeMo-RL recipe plus its infra config and return both. + + Two supported layouts: + + * **Split**: pass ``infra_path`` to point at a dedicated infra YAML + (recommended for any non-trivial infra block — keeps the recipe + focused on training config). The recipe must not also declare + an ``infra:`` key in that case. + * **Bundled**: recipe has an ``infra:`` top-level key. ``infra_path`` + is ``None``. + + Args: + recipe_path: Path to a recipe YAML (absolute or relative to cwd). + overrides: Hydra-style overrides. ``infra.*`` overrides apply to + the infra layer; other overrides apply to the recipe. + infra_path: Optional path to a standalone infra YAML (see above). + """ + overrides = overrides or [] + recipe_path = Path(recipe_path).resolve() + + recipe_overrides, infra_overrides = _partition_overrides(overrides) + recipe = _load_recipe(recipe_path, overrides=recipe_overrides) + + infra_raw = _merge_infra( + recipe, + infra_path=Path(infra_path).resolve() if infra_path else None, + overrides=infra_overrides, + ) + recipe.pop("infra", None) # peel any recipe-level infra: off + + # Validate; OmegaConf -> plain containers -> pydantic. + infra_container = OmegaConf.to_container(infra_raw, resolve=True) + if not infra_container.get("namespace"): + infra_container["namespace"] = _infer_kube_namespace() + infra = InfraConfig.model_validate(infra_container) + + return LoadedConfig(recipe=recipe, infra=infra, source_path=recipe_path) + + +_SA_NS_PATH = Path("/var/run/secrets/kubernetes.io/serviceaccount/namespace") + + +def _infer_kube_namespace() -> str: + """Default ``infra.namespace`` to the active kube context's namespace. + + Tries (in order) the pod service-account file, the current kubeconfig + context, and finally ``default``. + """ + try: + if _SA_NS_PATH.exists(): + ns = _SA_NS_PATH.read_text().strip() + if ns: + return ns + except OSError: + pass + try: + from kubernetes import config as k8s_config + + _contexts, active = k8s_config.list_kube_config_contexts() + ns = ((active or {}).get("context") or {}).get("namespace") + if ns: + return ns + except Exception: + pass + return "default" + + +def _partition_overrides(overrides: list[str]) -> tuple[list[str], list[str]]: + """Split Hydra overrides: infra.* go to the infra layer, rest to recipe. + + Keeps the recipe loader from seeing (and rejecting) infra.* keys on + strict NeMo-RL configs that use ``struct`` mode. + """ + recipe_side: list[str] = [] + infra_side: list[str] = [] + for o in overrides: + body = o.lstrip("+~") + if body.startswith("infra.") or body == "infra": + # Strip "infra." prefix so the override applies directly on + # the infra DictConfig (which has no wrapping "infra:" key). + leading = o[: len(o) - len(body)] + infra_side.append(leading + body[len("infra.") :]) + else: + recipe_side.append(o) + return recipe_side, infra_side + + +# ============================================================================= +# Internals +# ============================================================================= + + +def _load_recipe(recipe_path: Path, overrides: list[str]) -> DictConfig: + """Load a recipe YAML. Uses nemo_rl's loader if available, else OmegaConf fallback.""" + try: + from nemo_rl.utils.config import ( # type: ignore[import-not-found] + load_config, + parse_hydra_overrides, + register_omegaconf_resolvers, + ) + except ImportError: + return _load_recipe_fallback(recipe_path, overrides) + + register_omegaconf_resolvers() + cfg = load_config(str(recipe_path)) + + if overrides: + cfg = parse_hydra_overrides(cfg, overrides) + + if not isinstance(cfg, DictConfig): + raise ValueError(f"recipe at {recipe_path} did not load as a mapping") + return cfg + + +def _load_recipe_fallback(recipe_path: Path, overrides: list[str]) -> DictConfig: + """OmegaConf-only loader used when nemo_rl is unavailable. + + Handles ``defaults:`` inheritance recursively (same semantics as + :func:`nemo_rl.utils.config.load_config_with_inheritance`). Custom + resolvers like ``${mul:...}`` are NOT registered — those are only + needed by NeMo-RL's own entrypoints, which always run inside the Ray + container where nemo_rl is importable. + """ + cfg = _load_with_inheritance(recipe_path) + if overrides: + cfg = OmegaConf.merge(cfg, OmegaConf.from_dotlist(overrides)) + if not isinstance(cfg, DictConfig): + raise ValueError(f"recipe at {recipe_path} did not load as a mapping") + return cfg + + +def _load_with_inheritance(path: Path) -> DictConfig: + """Walk a recipe's ``defaults:`` chain and return the merged DictConfig.""" + cfg = OmegaConf.load(path) + if not isinstance(cfg, DictConfig): + raise ValueError(f"{path} did not load as a mapping") + + if "defaults" in cfg: # type: ignore[operator] + raw = cfg.pop("defaults") + defaults: list[str] = ( + [str(raw)] if isinstance(raw, (str, Path)) else [str(x) for x in raw] + ) + base: DictConfig = OmegaConf.create({}) + for rel in defaults: + parent = (path.parent / rel).resolve() + parent_cfg = _load_with_inheritance(parent) + merged = OmegaConf.merge(base, parent_cfg) + if not isinstance(merged, DictConfig): + raise ValueError(f"defaults merge for {path} produced non-mapping") + base = merged + merged = OmegaConf.merge(base, cfg) + if not isinstance(merged, DictConfig): + raise ValueError(f"inheritance merge for {path} produced non-mapping") + cfg = merged + return cfg + + +def _merge_infra( + recipe: DictConfig, + *, + infra_path: Path | None = None, + overrides: list[str] | None = None, +) -> DictConfig: + """Stack shipped defaults < user defaults < (infra file | recipe.infra) < CLI.""" + shipped = _load_yaml_if_present(_SHIPPED_DEFAULTS, required=True) + user = _load_yaml_if_present(_USER_DEFAULTS, required=False) + + infra_layer: DictConfig + if infra_path is not None: + if "infra" in recipe: # type: ignore[operator] + raise ValueError( + "infra config supplied via --infra but the recipe also contains " + "an `infra:` key — choose one or the other." + ) + infra_layer = _load_with_inheritance(infra_path) + infra_layer = _pick_infra(infra_layer) + else: + infra_layer = _extract_recipe_infra(recipe) + + # Strip top-level keys starting with "_" — these are anchor-only scratch + # sections (e.g. `_shared: &foo ...`) that YAML emits to the parsed dict + # even though they carry no infra meaning. + for k in list(infra_layer.keys()): # type: ignore[union-attr] + if isinstance(k, str) and k.startswith("_"): + infra_layer.pop(k) + + merged = OmegaConf.merge( + _pick_infra(shipped), + _pick_infra(user) if user is not None else OmegaConf.create({}), + infra_layer, + ) + if overrides: + merged = OmegaConf.merge(merged, OmegaConf.from_dotlist(overrides)) + + if not isinstance(merged, DictConfig): + raise RuntimeError("internal: infra merge did not produce a DictConfig") + return merged + + +def _pick_infra(cfg: DictConfig) -> DictConfig: + """A defaults file may be either ``{infra: {...}}`` or just the infra body.""" + if "infra" in cfg: # type: ignore[operator] + inner = cfg["infra"] + if not isinstance(inner, DictConfig): + raise ValueError("defaults file has non-mapping `infra:` key") + return inner + return cfg + + +def _extract_recipe_infra(recipe: DictConfig) -> DictConfig: + if "infra" not in recipe: # type: ignore[operator] + return OmegaConf.create({}) + inner = recipe["infra"] + if not isinstance(inner, DictConfig): + raise ValueError("recipe `infra:` key must be a mapping") + return inner + + +def _load_yaml_if_present(path: Path, *, required: bool) -> DictConfig | None: + if not path.exists(): + if required: + raise FileNotFoundError(f"shipped defaults missing: {path}") + return None + loaded = OmegaConf.load(path) + if not isinstance(loaded, DictConfig): + raise ValueError(f"{path} did not load as a mapping") + return loaded + + +__all__ = ["LoadedConfig", "load_recipe_with_infra"] diff --git a/tools/nrl_k8s/src/nrl_k8s/defaults/defaults.example.yaml b/tools/nrl_k8s/src/nrl_k8s/defaults/defaults.example.yaml new file mode 100644 index 00000000000..83c18d4cef7 --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/defaults/defaults.example.yaml @@ -0,0 +1,73 @@ +# Default `infra:` block shipped with nrl-k8s. +# +# Users can override any field with: +# 1. A recipe-level `infra:` mapping +# 2. A per-user file at ~/.config/nrl-k8s/defaults.yaml +# 3. CLI Hydra-style overrides: `infra.scheduler.queue=my-queue` +# +# Priority (low -> high): this file < user defaults < recipe < CLI overrides. +infra: + # namespace, image are REQUIRED and must be set per-cluster; no default is sensible. + # serviceAccount is optional — when set, the CLI patches it onto every pod template. + # namespace: "" + # image: "" + imagePullSecrets: [] + serviceAccount: null + scheduler: + kind: default + queue: null + placement: + nodeSelector: {} + tolerations: [] + networking: + hostNetwork: false + gloo_socket_ifname: null + nccl_socket_ifname: null + nccl_ib_disable: false + nccl_net: null + extra_env: {} + workspace: + kind: rayUpload + pvcName: null + mountPath: /mnt/nemo-rl + repoSubdir: workdirs + size: null + hostPath: null + hf_cache: + kind: none + pvcName: null + mountPath: /root/.cache/huggingface + checkpoints: + kind: none + pvcName: null + mountPath: /mnt/nemo-rl/checkpoints + submit: + kind: sdk + portForward: auto + devPod: auto + localDashboardPort: 18265 + launch: + mode: single + attach: + generation: null + gym: null + training: null + peerWatcher: true + resources: + training: + head: {cpu: null, memory: null, gpu: null} + worker: {cpu: null, memory: null, gpu: null} + generation: + head: {cpu: null, memory: null, gpu: null} + worker: {cpu: null, memory: null, gpu: null} + gym: + head: {cpu: null, memory: null, gpu: null} + worker: {cpu: null, memory: null, gpu: null} + # Per-role RayCluster topology. Unset roles cannot be brought up via + # `nrl-k8s cluster up --role `. + clusters: + training: null + generation: null + gym: null + labels: {} + annotations: {} diff --git a/tools/nrl_k8s/src/nrl_k8s/inspect.py b/tools/nrl_k8s/src/nrl_k8s/inspect.py new file mode 100644 index 00000000000..17846ca045d --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/inspect.py @@ -0,0 +1,179 @@ +"""Read-only introspection of the clusters a recipe owns. + +Used by ``nrl-k8s status`` and ``nrl-k8s logs`` to summarise what's running +and stream logs from it. Everything here is idempotent — no apply, no +delete, no job submission. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Iterator + +from kubernetes import client + +from . import k8s, submit +from .config import LoadedConfig +from .orchestrate import ALL_ROLES +from .schema import ClusterSpec, InfraConfig + + +@dataclass +class RayClusterPods: + head_name: str | None = None + head_phase: str | None = None + worker_names: list[str] = field(default_factory=list) + worker_phases: list[str] = field(default_factory=list) + + +@dataclass +class ClusterStatus: + role: str + name: str + state: str # "ready" | "—" | other KubeRay states + head_pod: str | None + head_phase: str | None # "Running" | "Pending" | ... + worker_phases: list[str] # one per worker pod + daemon_submission_id: str | None + daemon_status: str | None # Ray JobStatus string, if reachable + + +# ============================================================================= +# Status +# ============================================================================= + + +def collect_status(loaded: LoadedConfig) -> list[ClusterStatus]: + """Build a :class:`ClusterStatus` for every role declared in the recipe.""" + out: list[ClusterStatus] = [] + infra = loaded.infra + for role in ALL_ROLES: + cluster: ClusterSpec | None = getattr(infra.clusters, role) + if cluster is None: + continue + out.append(_status_for(role, cluster, infra)) + return out + + +def _status_for(role: str, cluster: ClusterSpec, infra: InfraConfig) -> ClusterStatus: + obj = k8s.get_raycluster(cluster.name, infra.namespace) + state = (obj or {}).get("status", {}).get("state", "—") if obj else "(not found)" + + pods = list_cluster_pods(cluster.name, infra.namespace) + + daemon_id: str | None = None + daemon_status: str | None = None + if obj is not None and state == "ready" and cluster.daemon is not None: + daemon_id, daemon_status = _latest_daemon_job( + cluster.name, infra.namespace, cluster.daemon.submissionId + ) + + return ClusterStatus( + role=role, + name=cluster.name, + state=state, + head_pod=pods.head_name, + head_phase=pods.head_phase, + worker_phases=pods.worker_phases, + daemon_submission_id=daemon_id, + daemon_status=daemon_status, + ) + + +def list_cluster_pods(cluster_name: str, namespace: str) -> RayClusterPods: + """Return the head and worker pods for a RayCluster.""" + k8s.load_kubeconfig() + core = client.CoreV1Api() + out = RayClusterPods() + for p in core.list_namespaced_pod( + namespace=namespace, label_selector=f"ray.io/cluster={cluster_name}" + ).items: + kind = (p.metadata.labels or {}).get("ray.io/node-type", "") + if kind == "head": + out.head_name = p.metadata.name + out.head_phase = p.status.phase + else: + out.worker_names.append(p.metadata.name) + out.worker_phases.append(p.status.phase) + return out + + +def _latest_daemon_job( + cluster_name: str, namespace: str, base_submission_id: str +) -> tuple[str | None, str | None]: + """Find the most recent Ray Job whose submission_id matches the base id + or a ``--replace``-suffixed variant (``-``), and return + its (submission_id, status) pair. Returns (None, None) on any error. + """ + try: + from ray.job_submission import JobSubmissionClient + + with submit.dashboard_url(cluster_name, namespace) as dash: + clnt = JobSubmissionClient(dash) + matches = [ + j + for j in clnt.list_jobs() + if j.submission_id == base_submission_id + or j.submission_id.startswith(f"{base_submission_id}-") + ] + if not matches: + return (base_submission_id, None) + latest = max(matches, key=lambda j: j.start_time or 0) + return (latest.submission_id, latest.status.value) + except Exception: + return (base_submission_id, None) + + +# ============================================================================= +# Logs +# ============================================================================= + + +def stream_pod_logs( + pod_name: str, + namespace: str, + *, + container: str | None = None, + follow: bool = False, + tail_lines: int | None = 200, +) -> Iterator[str]: + """Stream stdout/stderr from a specific pod.""" + k8s.load_kubeconfig() + core = client.CoreV1Api() + stream = core.read_namespaced_pod_log( + name=pod_name, + namespace=namespace, + container=container, + follow=follow, + tail_lines=tail_lines, + _preload_content=False, + ) + try: + for raw in stream.stream(): + yield ( + raw.decode("utf-8", errors="replace") + if isinstance(raw, (bytes, bytearray)) + else raw + ) + finally: + stream.release_conn() + + +def head_pod_name(cluster_name: str, namespace: str) -> str: + """Return the head pod's name for a RayCluster, raising if missing.""" + pods = list_cluster_pods(cluster_name, namespace) + if pods.head_name is None: + raise RuntimeError( + f"no head pod found for RayCluster {cluster_name} in {namespace}" + ) + return pods.head_name + + +__all__ = [ + "ClusterStatus", + "RayClusterPods", + "collect_status", + "head_pod_name", + "list_cluster_pods", + "stream_pod_logs", +] diff --git a/tools/nrl_k8s/src/nrl_k8s/k8s.py b/tools/nrl_k8s/src/nrl_k8s/k8s.py new file mode 100644 index 00000000000..fe9023d2dfd --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/k8s.py @@ -0,0 +1,196 @@ +"""Thin wrapper around the official ``kubernetes`` Python client. + +RayCluster + RayJob are Kubernetes ``CustomObjectsApi`` resources, so we use +the client's generic CR helpers instead of modeling either CRD in Python. +The research-facing YAML stays authoritative; this module just ships those +objects into the cluster and polls until they're ready. +""" + +from __future__ import annotations + +import functools +import time +from typing import Any + +from kubernetes import client, config +from kubernetes.client.exceptions import ApiException + +from ._logging import redact +from ._retry import with_retries + +# KubeRay CRD identifiers (stable since v1.x). +RAY_GROUP = "ray.io" +RAY_VERSION = "v1" +RAYCLUSTER_PLURAL = "rayclusters" + + +# ============================================================================= +# Client bootstrap +# ============================================================================= + + +@functools.cache +def load_kubeconfig() -> None: + """Pick the right config source (in-cluster vs kubeconfig) exactly once.""" + try: + config.load_incluster_config() + except config.ConfigException: + config.load_kube_config() + + +def custom_objects_api() -> client.CustomObjectsApi: + load_kubeconfig() + return client.CustomObjectsApi() + + +# ============================================================================= +# RayCluster lifecycle +# ============================================================================= + + +def apply_raycluster(manifest: dict[str, Any], namespace: str) -> dict[str, Any]: + """Create-or-replace a RayCluster. Returns the server-side object.""" + name = manifest["metadata"]["name"] + api = custom_objects_api() + try: + return with_retries( + lambda: api.create_namespaced_custom_object( + group=RAY_GROUP, + version=RAY_VERSION, + namespace=namespace, + plural=RAYCLUSTER_PLURAL, + body=manifest, + ) + ) + except ApiException as exc: + if exc.status == 409: + # Already exists — patch the spec in place (kubectl apply-equivalent). + return with_retries( + lambda: api.patch_namespaced_custom_object( + group=RAY_GROUP, + version=RAY_VERSION, + namespace=namespace, + plural=RAYCLUSTER_PLURAL, + name=name, + body=manifest, + ) + ) + # Attach a redacted manifest summary for easier debugging without + # leaking secret env values into the CLI output. + exc.nrl_k8s_manifest = redact(manifest) # type: ignore[attr-defined] + raise + + +def delete_raycluster( + name: str, namespace: str, *, ignore_missing: bool = True +) -> None: + api = custom_objects_api() + try: + with_retries( + lambda: api.delete_namespaced_custom_object( + group=RAY_GROUP, + version=RAY_VERSION, + namespace=namespace, + plural=RAYCLUSTER_PLURAL, + name=name, + ) + ) + except ApiException as exc: + if exc.status == 404 and ignore_missing: + return + raise + + +def get_raycluster(name: str, namespace: str) -> dict[str, Any] | None: + api = custom_objects_api() + try: + return with_retries( + lambda: api.get_namespaced_custom_object( + group=RAY_GROUP, + version=RAY_VERSION, + namespace=namespace, + plural=RAYCLUSTER_PLURAL, + name=name, + ) + ) + except ApiException as exc: + if exc.status == 404: + return None + raise + + +def list_rayclusters(namespace: str, label_selector: str | None = None) -> list[dict]: + api = custom_objects_api() + resp = with_retries( + lambda: api.list_namespaced_custom_object( + group=RAY_GROUP, + version=RAY_VERSION, + namespace=namespace, + plural=RAYCLUSTER_PLURAL, + label_selector=label_selector or "", + ) + ) + return resp.get("items", []) + + +def wait_for_raycluster_ready( + name: str, namespace: str, *, timeout_s: int = 900, poll_s: int = 5 +) -> None: + """Block until ``.status.state == ready`` or time out. + + KubeRay flips this flag once the head pod is up and all declared workers + report Running — the correct signal before submitting jobs. + """ + deadline = time.monotonic() + timeout_s + state: str | None = None + while time.monotonic() < deadline: + # get_raycluster already retries transient 5xx/timeout, so a poll + # blip won't abort the wait. + obj = get_raycluster(name, namespace) + state = (obj or {}).get("status", {}).get("state") + if state == "ready": + return + time.sleep(poll_s) + raise TimeoutError( + f"RayCluster {name} in {namespace} never reached state=ready " + f"(last seen: {state!r}) after {timeout_s}s" + ) + + +def wait_for_raycluster_gone( + name: str, namespace: str, *, timeout_s: int = 600, poll_s: int = 3 +) -> None: + deadline = time.monotonic() + timeout_s + while time.monotonic() < deadline: + if get_raycluster(name, namespace) is None: + return + time.sleep(poll_s) + raise TimeoutError(f"RayCluster {name} not deleted after {timeout_s}s") + + +def delete_configmap(name: str, namespace: str, *, ignore_missing: bool = True) -> bool: + """Delete a ConfigMap. Returns True if deleted, False if it didn't exist.""" + load_kubeconfig() + core = client.CoreV1Api() + try: + with_retries( + lambda: core.delete_namespaced_config_map(name=name, namespace=namespace) + ) + return True + except ApiException as exc: + if exc.status == 404 and ignore_missing: + return False + raise + + +__all__ = [ + "apply_raycluster", + "custom_objects_api", + "delete_configmap", + "delete_raycluster", + "get_raycluster", + "list_rayclusters", + "load_kubeconfig", + "wait_for_raycluster_gone", + "wait_for_raycluster_ready", +] diff --git a/tools/nrl_k8s/src/nrl_k8s/manifest.py b/tools/nrl_k8s/src/nrl_k8s/manifest.py new file mode 100644 index 00000000000..6c9c10b9c91 --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/manifest.py @@ -0,0 +1,104 @@ +"""Build a RayCluster manifest dict from the recipe's inline ``spec``. + +The recipe encodes the full RayCluster shape inline under +``infra.clusters..spec`` — this module just wraps it in the standard +``apiVersion/kind/metadata`` envelope and patches three cross-cutting +fields (``image`` on every container, ``imagePullSecrets`` on every pod +template, optional ``serviceAccountName``) from the top-level ``infra`` +block so you don't repeat them across roles. + +The resulting dict is submitted as-is via the official ``kubernetes`` +Python client's ``CustomObjectsApi``. +""" + +from __future__ import annotations + +import copy +from typing import Any + +from .schema import ClusterSpec, InfraConfig + +# ============================================================================= +# Public API +# ============================================================================= + + +def build_raycluster_manifest( + cluster: ClusterSpec, infra: InfraConfig +) -> dict[str, Any]: + """Build the full RayCluster dict for apply. + + Args: + cluster: the role's ClusterSpec (name + inline spec + optional daemon). + infra: top-level InfraConfig — supplies namespace, image, pull secrets, + optional serviceAccount. These are patched into every container / + pod template in the spec. + + Returns: + A dict suitable for ``CustomObjectsApi.create_namespaced_custom_object``. + """ + spec = copy.deepcopy(cluster.spec) + + _patch_images(spec, infra.image) + _patch_image_pull_secrets(spec, list(infra.imagePullSecrets)) + if infra.serviceAccount is not None: + _patch_service_account(spec, infra.serviceAccount) + + metadata: dict[str, Any] = { + "name": cluster.name, + "namespace": infra.namespace, + } + labels = {**infra.labels, **cluster.labels} + annotations = {**infra.annotations, **cluster.annotations} + if labels: + metadata["labels"] = labels + if annotations: + metadata["annotations"] = annotations + + return { + "apiVersion": "ray.io/v1", + "kind": "RayCluster", + "metadata": metadata, + "spec": spec, + } + + +# ============================================================================= +# Internals +# ============================================================================= + + +def _walk_pod_templates(raycluster_spec: dict) -> list[dict]: + """Return every PodSpec inside a RayCluster (head + all worker groups).""" + specs: list[dict] = [] + head = raycluster_spec.get("headGroupSpec") or {} + head_spec = head.get("template", {}).get("spec") + if isinstance(head_spec, dict): + specs.append(head_spec) + for wg in raycluster_spec.get("workerGroupSpecs") or []: + wg_spec = wg.get("template", {}).get("spec") + if isinstance(wg_spec, dict): + specs.append(wg_spec) + return specs + + +def _patch_images(raycluster_spec: dict, image: str) -> None: + for pod_spec in _walk_pod_templates(raycluster_spec): + for container in pod_spec.get("containers", []): + container["image"] = image + + +def _patch_image_pull_secrets(raycluster_spec: dict, secrets: list[str]) -> None: + if not secrets: + return + body = [{"name": s} for s in secrets] + for pod_spec in _walk_pod_templates(raycluster_spec): + pod_spec["imagePullSecrets"] = body + + +def _patch_service_account(raycluster_spec: dict, service_account: str) -> None: + for pod_spec in _walk_pod_templates(raycluster_spec): + pod_spec["serviceAccountName"] = service_account + + +__all__ = ["build_raycluster_manifest"] diff --git a/tools/nrl_k8s/src/nrl_k8s/orchestrate.py b/tools/nrl_k8s/src/nrl_k8s/orchestrate.py new file mode 100644 index 00000000000..482b5c793dd --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/orchestrate.py @@ -0,0 +1,336 @@ +"""One-shot orchestration for a disaggregated run. + +``nrl-k8s run `` delegates here. The flow: + + 1. For each role in order (generation, gym, training): + - Apply the RayCluster manifest. + - Wait for state=ready. + - If the role has a daemon entrypoint, stage a fresh working_dir, + submit the daemon as a Ray Job, and (if configured) wait on a + health-check URL. + 2. Stage a working_dir for training and submit ``infra.launch.entrypoint`` + as a Ray Job against the training cluster. + 3. Return the training job's submission ID. Callers tail logs separately. + +Every cluster-specific value — namespace, cluster names, image, ports, +entrypoints, node selectors — is read from the recipe. This module has no +hardcoded cluster assumptions. +""" + +from __future__ import annotations + +import re +import time +import urllib.error +import urllib.request +from dataclasses import dataclass +from pathlib import Path +from typing import Literal + +from omegaconf import OmegaConf +from ray.job_submission import JobStatus, JobSubmissionClient + +from . import k8s, submit, workdir +from .config import LoadedConfig +from .manifest import build_raycluster_manifest +from .schema import ClusterSpec, InfraConfig + +Role = Literal["generation", "gym", "training"] +ALL_ROLES: tuple[Role, ...] = ("generation", "gym", "training") + + +@dataclass +class RunResult: + training_dashboard: str + training_job_id: str + + +# ============================================================================= +# Public API +# ============================================================================= + + +def _fresh_submission_id(base: str) -> str: + return f"{base}-{int(time.time())}" + + +def bring_up_cluster( + role: Role, + loaded: LoadedConfig, + *, + log: callable, + wait_ready: bool = True, + ready_timeout_s: int = 900, +) -> str: + """Apply the RayCluster for ``role`` and wait for it to be ready.""" + cluster = _require_cluster(loaded.infra, role) + manifest = build_raycluster_manifest(cluster, loaded.infra) + name = cluster.name + namespace = loaded.infra.namespace + + log(f"[{role}] applying RayCluster {name} in namespace {namespace}") + k8s.apply_raycluster(manifest, namespace) + + if wait_ready: + log(f"[{role}] waiting for RayCluster {name} to reach state=ready ...") + k8s.wait_for_raycluster_ready(name, namespace, timeout_s=ready_timeout_s) + log(f"[{role}] RayCluster {name} is ready.") + + return name + + +def submit_daemon( + role: Role, + loaded: LoadedConfig, + cluster_name: str, + *, + log: callable, + repo_root: Path, + replace: bool = False, +) -> str | None: + """If the role has a daemon spec, stage+submit it. Returns submission_id.""" + cluster = _require_cluster(loaded.infra, role) + daemon = cluster.daemon + if daemon is None: + return None + + namespace = loaded.infra.namespace + + # One port-forward for both the status check and the submit avoids a + # startup race where a separate check returns None while the forward + # boots. + with submit.dashboard_url(cluster_name, namespace) as dash: + client = JobSubmissionClient(dash) + + existing = None + if daemon.submissionId: + try: + existing = client.get_job_status(daemon.submissionId) + except Exception: + existing = None + + if existing in (JobStatus.RUNNING, JobStatus.SUCCEEDED) and not replace: + log( + f"[{role}] daemon {daemon.submissionId} already {existing.value} — skipping submit" + ) + return daemon.submissionId + + if existing in (JobStatus.FAILED, JobStatus.STOPPED) and not replace: + raise RuntimeError( + f"daemon {daemon.submissionId} is {existing.value} — " + f"re-run with --replace (or bump infra.clusters.{role}.daemon.submissionId)" + ) + + # Ray refuses to re-use a submissionId even after terminal state, so + # --replace picks a fresh suffix and stops the live one if any. + submission_id = daemon.submissionId + if replace and existing is not None: + if existing is JobStatus.RUNNING: + log(f"[{role}] --replace: stopping {daemon.submissionId}") + try: + client.stop_job(daemon.submissionId) + _wait_job_stopped(client, daemon.submissionId, log=log, role=role) + except Exception as exc: # noqa: BLE001 + log(f"[{role}] warning: stop failed: {exc}") + if daemon.submissionId: + submission_id = _fresh_submission_id(daemon.submissionId) + log(f"[{role}] --replace: using fresh submissionId {submission_id}") + + upload_paths = daemon.rayUploadPaths or _upload_paths(loaded.infra) + log(f"[{role}] staging working_dir for daemon ({len(upload_paths)} paths)") + wd = workdir.stage_workdir(repo_root, include_paths=upload_paths) + + log(f"[{role}] submitting daemon via {dash}") + job_id = submit.submit_ray_job( + dash, + entrypoint=daemon.entrypoint, + working_dir=wd, + env_vars=daemon.env, + submission_id=submission_id, + ) + log(f"[{role}] daemon submitted as job {job_id}") + if daemon.healthCheckUrl: + _wait_for_http(daemon.healthCheckUrl, daemon.healthCheckTimeoutS, log, role) + return job_id + + +def submit_training( + loaded: LoadedConfig, + *, + log: callable, + repo_root: Path, + replace: bool = False, +) -> RunResult: + """Stage + submit the training job against the training cluster.""" + infra = loaded.infra + launch = infra.launch + if not launch.entrypoint: + raise ValueError("infra.launch.entrypoint must be set for `nrl-k8s launch`") + + if replace: + _reset_endpoint_registry(loaded, log=log) + + cluster = _require_cluster(infra, "training") + name = cluster.name + + log("[training] staging working_dir ...") + recipe_yaml = OmegaConf.to_yaml(loaded.recipe) + wd = workdir.stage_workdir( + repo_root, + include_paths=_upload_paths(infra), + extra_files={"nrl_k8s_run.yaml": recipe_yaml}, + ) + + with submit.dashboard_url(name, infra.namespace) as dash: + # Training jobs have auto-generated submissionIds, so no ID collision; + # ``--replace`` just stops any RUNNING job on the cluster so the new + # one can claim GPUs. + if replace: + client = JobSubmissionClient(dash) + for job in client.list_jobs(): + if job.status is JobStatus.RUNNING: + log( + f"[training] --replace: stopping running job {job.submission_id}" + ) + try: + client.stop_job(job.submission_id) + _wait_job_stopped( + client, job.submission_id, log=log, role="training" + ) + except Exception as exc: # noqa: BLE001 + log(f"[training] warning: stop failed: {exc}") + + log(f"[training] submitting training job via {dash}") + job_id = submit.submit_ray_job( + dash, + entrypoint=launch.entrypoint, + working_dir=wd, + env_vars=launch.env, + ) + log(f"[training] training job submitted: {job_id}") + return RunResult(training_dashboard=dash, training_job_id=job_id) + + +def run( + loaded: LoadedConfig, + *, + log: callable, + repo_root: Path, + replace: bool = False, +) -> RunResult: + """Do the full sequence: bring up all 3 clusters + daemons, submit training.""" + if replace: + _reset_endpoint_registry(loaded, log=log) + + for role in ALL_ROLES: + if _get_cluster(loaded.infra, role) is None: + log(f"[{role}] not defined in recipe — skipping") + continue + name = bring_up_cluster(role, loaded, log=log) + submit_daemon(role, loaded, name, log=log, repo_root=repo_root, replace=replace) + + return submit_training(loaded, log=log, repo_root=repo_root, replace=replace) + + +_JOB_ID_RE = re.compile(r"--job-id[= ]+(\S+)") + + +def _infer_disagg_job_id(infra: InfraConfig) -> str | None: + """Best-effort extraction of the gym's ``--job-id`` from its entrypoint. + + The endpoint-registry ConfigMap is named ``nemo-rl-endpoints-``; + gym publishes ``gym_head_server`` there and training publishes + ``vllm_base_urls``. We parse the id from the gym daemon entrypoint so + ``--replace`` can delete the ConfigMap without a dedicated config key. + """ + gym = infra.clusters.gym + if gym is None or gym.daemon is None: + return None + m = _JOB_ID_RE.search(gym.daemon.entrypoint) + return m.group(1) if m else None + + +def _reset_endpoint_registry(loaded: LoadedConfig, *, log: callable) -> None: + """Delete the endpoint-registry ConfigMap so gym + training rendezvous + on fresh URLs instead of caching stragglers from a prior failed run. + """ + job_id = _infer_disagg_job_id(loaded.infra) + if not job_id: + return + cm_name = f"nemo-rl-endpoints-{job_id}" + if k8s.delete_configmap(cm_name, loaded.infra.namespace): + log(f"[replace] deleted endpoint registry ConfigMap {cm_name}") + + +# ============================================================================= +# Internals +# ============================================================================= + + +def _get_cluster(infra: InfraConfig, role: Role) -> ClusterSpec | None: + return getattr(infra.clusters, role) + + +def _require_cluster(infra: InfraConfig, role: Role) -> ClusterSpec: + cluster = _get_cluster(infra, role) + if cluster is None: + raise ValueError(f"infra.clusters.{role} is not defined") + return cluster + + +def _upload_paths(infra: InfraConfig) -> list[str]: + """Resolve the list of repo-relative paths to stage for Ray uploads.""" + if infra.launch.rayUploadPaths is not None: + return list(infra.launch.rayUploadPaths) + return list(workdir.DEFAULT_RAY_UPLOAD_PATHS) + + +_TERMINAL = (JobStatus.STOPPED, JobStatus.FAILED, JobStatus.SUCCEEDED) + + +def _wait_job_stopped( + client: JobSubmissionClient, + submission_id: str, + *, + log: callable, + role: Role, + timeout_s: int = 60, +) -> None: + """Block until a Ray Job reaches a terminal state after a stop_job call.""" + deadline = time.monotonic() + timeout_s + while time.monotonic() < deadline: + try: + status = client.get_job_status(submission_id) + except Exception: + return + if status in _TERMINAL: + log(f"[{role}] previous job {submission_id} → {status.value}") + return + time.sleep(2) + log( + f"[{role}] previous job {submission_id} did not stop within {timeout_s}s; continuing" + ) + + +def _wait_for_http(url: str, timeout_s: int, log: callable, role: Role) -> None: + log(f"[{role}] waiting for health-check {url} (timeout {timeout_s}s)") + deadline = time.monotonic() + timeout_s + while time.monotonic() < deadline: + try: + with urllib.request.urlopen(url, timeout=5) as r: + if 200 <= r.status < 500: + log(f"[{role}] health-check {url} responded {r.status}") + return + except (urllib.error.URLError, TimeoutError, OSError): + pass + time.sleep(5) + raise TimeoutError(f"health-check {url} did not respond within {timeout_s}s") + + +__all__ = [ + "RunResult", + "bring_up_cluster", + "run", + "submit_daemon", + "submit_training", +] diff --git a/tools/nrl_k8s/src/nrl_k8s/schema.py b/tools/nrl_k8s/src/nrl_k8s/schema.py new file mode 100644 index 00000000000..597793289ae --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/schema.py @@ -0,0 +1,397 @@ +"""Pydantic schema for the ``infra:`` section of a NeMo-RL recipe. + +A recipe YAML is a standard NeMo-RL recipe plus an optional top-level ``infra:`` +mapping. The CLI merges recipe-level ``infra:`` with a user-level defaults file +(``~/.config/nrl-k8s/defaults.yaml``) and a shipped defaults file +(``defaults/defaults.example.yaml``) before validating the result through +:class:`InfraConfig`. + +Strict validation (``extra='forbid'``) surfaces typos early. Every field that +isn't strictly cluster-identifying has a sensible default so short ``infra:`` +blocks work on well-configured clusters. +""" + +from __future__ import annotations + +from enum import Enum +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + + +class _StrictModel(BaseModel): + """Base pydantic model with strict extra-field rejection.""" + + model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) + + +# ============================================================================= +# Scheduling +# ============================================================================= + + +class SchedulerKind(str, Enum): + KAI = "kai" + KUEUE = "kueue" + DEFAULT = "default" + + +class SchedulerSpec(_StrictModel): + kind: SchedulerKind = SchedulerKind.DEFAULT + queue: str | None = None + + @model_validator(mode="after") + def _queue_required_for_kai_kueue(self) -> "SchedulerSpec": + if self.kind in (SchedulerKind.KAI, SchedulerKind.KUEUE) and not self.queue: + raise ValueError( + f"infra.scheduler.queue is required when scheduler.kind={self.kind.value}" + ) + return self + + +# ============================================================================= +# Placement (node selectors, tolerations) +# ============================================================================= + + +class Toleration(_StrictModel): + key: str + operator: Literal["Equal", "Exists"] = "Equal" + value: str | None = None + effect: Literal["NoSchedule", "PreferNoSchedule", "NoExecute"] = "NoSchedule" + tolerationSeconds: int | None = None + + +class PlacementSpec(_StrictModel): + nodeSelector: dict[str, str] = Field(default_factory=dict) + tolerations: list[Toleration] = Field(default_factory=list) + # Optional raw affinity passthrough (rarely needed; falls back to nodeSelector). + affinity: dict[str, Any] | None = None + + +# ============================================================================= +# Networking +# ============================================================================= + + +class NetworkingSpec(_StrictModel): + hostNetwork: bool = False + gloo_socket_ifname: str | None = None + nccl_socket_ifname: str | None = None + nccl_ib_disable: bool = False + nccl_net: Literal["Socket", "IB", "OFI"] | None = None + # Raw extra NCCL env vars if the cluster needs more; user-managed. + extra_env: dict[str, str] = Field(default_factory=dict) + + +# ============================================================================= +# Storage (workspace, HF cache, checkpoints) +# ============================================================================= + + +class WorkspaceKind(str, Enum): + LUSTRE = "lustre" # FSx for Lustre / managed-parallel-FS PVC + PVC = "pvc" # any other RWX PVC + HOST_PATH = "hostPath" # dev / kind only + RAY_UPLOAD = "rayUpload" # Ray Job SDK working_dir upload (fallback, 100 MiB cap) + AUTO = "auto" # prefer lustre if the pvc exists, else rayUpload + + +class WorkspaceSpec(_StrictModel): + kind: WorkspaceKind = WorkspaceKind.RAY_UPLOAD + # PVC-backed kinds (lustre, pvc) + pvcName: str | None = None + mountPath: str = "/mnt/nemo-rl" + repoSubdir: str = ( + "workdirs" # ${mountPath}/${repoSubdir}// holds the synced repo + ) + size: str | None = None # only consulted if the PVC needs to be created (lustre) + # hostPath-backed + hostPath: str | None = None + + @model_validator(mode="after") + def _required_fields_by_kind(self) -> "WorkspaceSpec": + if self.kind in (WorkspaceKind.LUSTRE, WorkspaceKind.PVC) and not self.pvcName: + raise ValueError( + f"infra.workspace.pvcName is required when kind={self.kind.value}" + ) + if self.kind is WorkspaceKind.HOST_PATH and not self.hostPath: + raise ValueError("infra.workspace.hostPath is required when kind=hostPath") + return self + + +class HFCacheKind(str, Enum): + LUSTRE = "lustre" + PVC = "pvc" + EMPTY_DIR = "emptyDir" + NONE = "none" + + +class HFCacheSpec(_StrictModel): + kind: HFCacheKind = HFCacheKind.NONE + pvcName: str | None = None + mountPath: str = "/root/.cache/huggingface" + + @model_validator(mode="after") + def _pvc_required(self) -> "HFCacheSpec": + if self.kind in (HFCacheKind.LUSTRE, HFCacheKind.PVC) and not self.pvcName: + raise ValueError( + f"infra.hf_cache.pvcName is required when kind={self.kind.value}" + ) + return self + + +class CheckpointsKind(str, Enum): + LUSTRE = "lustre" + PVC = "pvc" + NONE = "none" # checkpoints land on pod-local storage (smoke tests only) + + +class CheckpointsSpec(_StrictModel): + kind: CheckpointsKind = CheckpointsKind.NONE + pvcName: str | None = None + mountPath: str = "/mnt/nemo-rl/checkpoints" + + @model_validator(mode="after") + def _pvc_required(self) -> "CheckpointsSpec": + if ( + self.kind in (CheckpointsKind.LUSTRE, CheckpointsKind.PVC) + and not self.pvcName + ): + raise ValueError( + f"infra.checkpoints.pvcName is required when kind={self.kind.value}" + ) + return self + + +# ============================================================================= +# Submission (how the CLI gets a job onto the cluster) +# ============================================================================= + + +class SubmitKind(str, Enum): + SDK = "sdk" # Ray Job SDK (default) + RAYJOB = "rayjob" # RayJob CRD + + +class PortForwardMode(str, Enum): + KUBECTL_RAY_PLUGIN = "kubectl-ray-plugin" + KUBECTL_PORT_FORWARD = "kubectl-port-forward" + AUTO = "auto" + + +class DevPodMode(str, Enum): + AUTO = "auto" + REQUIRED = "required" + SKIP = "skip" + + +class SubmitSpec(_StrictModel): + kind: SubmitKind = SubmitKind.SDK + portForward: PortForwardMode = PortForwardMode.AUTO + devPod: DevPodMode = DevPodMode.AUTO + # Local port when port-forwarding; default avoids collision with `kubectl-ray session`. + localDashboardPort: int = 18265 + + +# ============================================================================= +# Launch (single vs disaggregated, attach mode) +# ============================================================================= + + +class LaunchMode(str, Enum): + SINGLE = "single" # colocated training in one RayCluster (default) + RAYJOB = "rayjob" # ephemeral cluster per run (auto teardown) + ATTACH = "attach" # submit training onto existing RayClusters + BRINGUP = "bringup" # create a long-lived RayCluster, no job + + +class AttachSpec(_StrictModel): + generation: str | None = None + gym: str | None = None + training: str | None = None # null = create ephemeral training cluster + + +class LaunchSpec(_StrictModel): + mode: LaunchMode = LaunchMode.SINGLE + attach: AttachSpec = Field(default_factory=AttachSpec) + peerWatcher: bool = True + # Shell command the training job runs inside the Ray cluster. Required + # for `nrl-k8s launch` / `nrl-k8s run`. Typically a line like + # ``python -u examples/.../entry.py --config nrl_k8s_run.yaml ...``. + # The CLI stages the resolved recipe as ``nrl_k8s_run.yaml`` at the + # working_dir root so this command can reference it by name. + entrypoint: str | None = None + # Env vars injected into the training job's runtime_env. + env: dict[str, str] = Field(default_factory=dict) + # Repo-relative paths to stage into every Ray Job's working_dir. + # None means "use the built-in default" (see nrl_k8s.workdir). Keeping + # this narrow matters — Ray caps working_dir uploads at 100 MiB, so + # recipes should exclude datasets they don't need for the run. + rayUploadPaths: list[str] | None = None + + @model_validator(mode="after") + def _attach_fields(self) -> "LaunchSpec": + if self.mode is LaunchMode.ATTACH: + if not (self.attach.generation or self.attach.gym or self.attach.training): + raise ValueError( + "infra.launch.mode=attach requires at least one of " + "infra.launch.attach.{generation,gym,training}" + ) + return self + + +# ============================================================================= +# Resource profiles per role (CLI derives sensible defaults from cluster.*) +# ============================================================================= + + +class PodResources(_StrictModel): + cpu: str | None = None # e.g. "8" or "500m" + memory: str | None = None # e.g. "32Gi" + # nvidia.com/gpu is derived from cluster.gpus_per_node by default, but can be overridden. + gpu: int | None = None + + +class ResourceProfile(_StrictModel): + head: PodResources = Field(default_factory=PodResources) + worker: PodResources = Field(default_factory=PodResources) + + +class ResourcesSpec(_StrictModel): + training: ResourceProfile = Field(default_factory=ResourceProfile) + generation: ResourceProfile = Field(default_factory=ResourceProfile) + gym: ResourceProfile = Field(default_factory=ResourceProfile) + + +# ============================================================================= +# Per-role cluster spec — a pointer to a raw RayCluster manifest on disk. +# +# We deliberately don't model the RayCluster topology in pydantic. Researchers +# already maintain these YAMLs (`infra/examples/disagg_*_raycluster.yaml`); +# the CLI just reads, optionally patches a handful of fields, and applies +# them via the official kubernetes Python client. The manifest is the +# authoritative source for head/worker shape, ports, node placement, etc. +# ============================================================================= + + +class DaemonSpec(_StrictModel): + """A long-running Ray Job to submit once the RayCluster is ready. + + The entrypoint runs via ``ray.job_submission.JobSubmissionClient`` against + the cluster's dashboard. ``submissionId`` is the human-readable name we + tag the job with so subsequent ``nrl-k8s job logs`` can find it. + """ + + entrypoint: str + submissionId: str | None = None + # Environment variables to inject into the Ray job runtime. + env: dict[str, str] = Field(default_factory=dict) + # Health-check URL (cluster-internal DNS). If set, the CLI polls it after + # submission so `cluster up` returns only once the daemon is serving. + healthCheckUrl: str | None = None + # Seconds to wait on the health-check before giving up. + healthCheckTimeoutS: int = 300 + # Per-daemon override for the working_dir upload set. None = use + # ``infra.launch.rayUploadPaths``. Use this to keep each daemon's + # upload small (e.g. the gen server doesn't need training data). + rayUploadPaths: list[str] | None = None + + +class ClusterSpec(_StrictModel): + """One long-lived RayCluster in the disaggregated setup. + + ``spec`` is the inline RayCluster ``.spec`` body — the CLI wraps it in + ``apiVersion: ray.io/v1``, ``kind: RayCluster``, ``metadata.name/namespace`` + and patches cross-cutting fields (image, imagePullSecrets, serviceAccount) + from the top-level ``infra`` keys before applying. + + We deliberately do *not* model the RayCluster topology in pydantic — + it's a free-form dict so every upstream RayCluster field works without + schema changes. ``extra='forbid'`` still catches typos in the + surrounding keys (``name``, ``daemon``). + """ + + name: str + spec: dict[str, Any] + # Labels/annotations to attach to metadata (useful for KAI/Kyverno). + labels: dict[str, str] = Field(default_factory=dict) + annotations: dict[str, str] = Field(default_factory=dict) + # Daemon to start on the cluster once Ready (e.g. the gen/gym server). + daemon: DaemonSpec | None = None + + +class ClustersSpec(_StrictModel): + """The 3 named RayClusters in a disaggregated run.""" + + training: ClusterSpec | None = None + generation: ClusterSpec | None = None + gym: ClusterSpec | None = None + + +# ============================================================================= +# Top-level InfraConfig +# ============================================================================= + + +class InfraConfig(_StrictModel): + # Cluster-identifying (required) + namespace: str + image: str + imagePullSecrets: list[str] = Field(default_factory=list) + # Ray version pinned on the cluster; optional — the CLI uses the image default. + rayVersion: str | None = None + # Pod ServiceAccount; None means "don't patch — keep whatever the manifest has". + serviceAccount: str | None = None + + # Behaviour + scheduler: SchedulerSpec = Field(default_factory=SchedulerSpec) + placement: PlacementSpec = Field(default_factory=PlacementSpec) + networking: NetworkingSpec = Field(default_factory=NetworkingSpec) + workspace: WorkspaceSpec = Field(default_factory=WorkspaceSpec) + hf_cache: HFCacheSpec = Field(default_factory=HFCacheSpec) + checkpoints: CheckpointsSpec = Field(default_factory=CheckpointsSpec) + submit: SubmitSpec = Field(default_factory=SubmitSpec) + launch: LaunchSpec = Field(default_factory=LaunchSpec) + resources: ResourcesSpec = Field(default_factory=ResourcesSpec) + clusters: ClustersSpec = Field(default_factory=ClustersSpec) + + # Opaque extra labels / annotations the platform may require (Kyverno-enforced, etc.) + labels: dict[str, str] = Field(default_factory=dict) + annotations: dict[str, str] = Field(default_factory=dict) + + @field_validator("namespace", "image") + @classmethod + def _not_blank(cls, v: str) -> str: + if not v.strip(): + raise ValueError("must not be empty") + return v + + +__all__ = [ + "AttachSpec", + "CheckpointsKind", + "CheckpointsSpec", + "ClusterSpec", + "ClustersSpec", + "DaemonSpec", + "DevPodMode", + "HFCacheKind", + "HFCacheSpec", + "InfraConfig", + "LaunchMode", + "LaunchSpec", + "NetworkingSpec", + "PlacementSpec", + "PodResources", + "PortForwardMode", + "ResourceProfile", + "ResourcesSpec", + "SchedulerKind", + "SchedulerSpec", + "SubmitKind", + "SubmitSpec", + "Toleration", + "WorkspaceKind", + "WorkspaceSpec", +] diff --git a/tools/nrl_k8s/src/nrl_k8s/submit.py b/tools/nrl_k8s/src/nrl_k8s/submit.py new file mode 100644 index 00000000000..da0a58ad597 --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/submit.py @@ -0,0 +1,326 @@ +"""Submit a Ray Job to a named RayCluster. + +Two dashboard access modes: + +* **In-cluster**: when the CLI runs inside a pod that can reach the cluster + DNS (``-head-svc..svc.cluster.local:8265``), we hit the + dashboard directly. Detected via ``KUBERNETES_SERVICE_HOST``. + +* **Laptop**: spawn a ``kubectl port-forward`` subprocess to forward + ``-head-svc:8265`` to a local port and submit via + ``http://127.0.0.1:``. The subprocess is killed on context exit. + +No other cluster-specific assumptions: namespace, cluster name, port are +all passed in. +""" + +from __future__ import annotations + +import contextlib +import os +import shutil +import socket +import subprocess +import threading +import time +from pathlib import Path +from typing import Any, Iterator + +DASHBOARD_PORT = 8265 # KubeRay convention (headGroupSpec containerPort). + + +# ============================================================================= +# kubectl preflight (cached) +# ============================================================================= + +# Cache the probe result for the lifetime of the CLI invocation — kubectl +# auth doesn't flip between one command's validate and apply. +_KUBECTL_OK: bool | None = None + + +def kubectl_preflight(namespace: str) -> None: + """Fail early with an actionable error if kubectl is missing or unauthenticated. + + Running two cheap checks up-front is much friendlier than watching a + port-forward die with "The connection to the server … was refused" 30s + later. Result is cached for the process so we don't re-probe on every + ``dashboard_url`` call inside one CLI invocation. + """ + global _KUBECTL_OK + if _KUBECTL_OK: + return + if shutil.which("kubectl") is None: + raise RuntimeError( + "kubectl not found on PATH — install it or run nrl-k8s from an " + "in-cluster pod." + ) + try: + subprocess.run( + ["kubectl", "version", "--client"], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + timeout=10, + ) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc: + raise RuntimeError( + "`kubectl version --client` failed — is kubectl installed correctly? " + f"({exc})" + ) from exc + try: + res = subprocess.run( + ["kubectl", "auth", "can-i", "get", "pods", "-n", namespace], + capture_output=True, + text=True, + timeout=10, + ) + except subprocess.TimeoutExpired as exc: + raise RuntimeError( + f"`kubectl auth can-i` timed out contacting the API server for " + f"namespace {namespace}; is the cluster reachable? (try `aws sso login`)" + ) from exc + verdict = (res.stdout or "").strip().lower() + if verdict != "yes": + raise RuntimeError( + f"kubectl has no permission to get pods in namespace {namespace} — " + f"authenticate (e.g. `aws sso login`) or request the edit role." + ) + _KUBECTL_OK = True + + +# ============================================================================= +# Public API +# ============================================================================= + + +def is_in_cluster() -> bool: + """Heuristic: are we running inside a Kubernetes pod?""" + return "KUBERNETES_SERVICE_HOST" in os.environ + + +class _PortForward: + """Manage a long-running ``kubectl port-forward`` with stdout drainage. + + The subprocess pipes are drained on a daemon thread so a long + ``tail_job_logs`` session doesn't block on a full pipe buffer. + ``restart()`` is available so HTTP clients can recover from a forward + that died mid-call without tearing down the whole context. + """ + + def __init__(self, cluster_name: str, namespace: str, port: int) -> None: + self.cluster_name = cluster_name + self.namespace = namespace + self.port = port + self._proc: subprocess.Popen | None = None + self._drain: threading.Thread | None = None + self._stop = threading.Event() + + def start(self) -> None: + cmd = [ + "kubectl", + "-n", + self.namespace, + "port-forward", + f"svc/{self.cluster_name}-head-svc", + f"{self.port}:{DASHBOARD_PORT}", + ] + self._proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + self._stop.clear() + self._drain = threading.Thread( + target=self._drain_pipe, name="nrl-k8s-pf-drain", daemon=True + ) + self._drain.start() + _wait_for_tcp("127.0.0.1", self.port, timeout_s=30, proc=self._proc) + + def _drain_pipe(self) -> None: + """Consume stdout so a long session doesn't stall on a full pipe.""" + if self._proc is None or self._proc.stdout is None: + return + try: + for _ in iter(self._proc.stdout.readline, ""): + if self._stop.is_set(): + return + except (ValueError, OSError): + # Pipe was closed under us during shutdown — not an error. + return + + def alive(self) -> bool: + return self._proc is not None and self._proc.poll() is None + + def restart(self) -> None: + """Kill the old process and start a new one on the same local port.""" + self.stop() + self.start() + + def stop(self) -> None: + self._stop.set() + proc = self._proc + if proc is None: + return + proc.terminate() + with contextlib.suppress(subprocess.TimeoutExpired): + proc.wait(timeout=5) + if proc.poll() is None: + proc.kill() + self._proc = None + + +@contextlib.contextmanager +def dashboard_url( + cluster_name: str, + namespace: str, + *, + local_port: int | None = None, +) -> Iterator[str]: + """Yield a URL to the dashboard, managing a port-forward if needed.""" + if is_in_cluster(): + yield f"http://{cluster_name}-head-svc.{namespace}.svc.cluster.local:{DASHBOARD_PORT}" + return + + if shutil.which("kubectl") is None: + raise RuntimeError( + "kubectl not found on PATH — install it or run nrl-k8s from an " + "in-cluster pod." + ) + + port = local_port or _free_port() + pf = _PortForward(cluster_name, namespace, port) + pf.start() + try: + yield f"http://127.0.0.1:{port}" + finally: + pf.stop() + + +def submit_ray_job( + dashboard: str, + *, + entrypoint: str, + working_dir: Path, + env_vars: dict[str, str] | None = None, + submission_id: str | None = None, + pip: list[str] | None = None, +) -> str: + """Submit a Ray Job. Returns the job submission ID.""" + from ray.job_submission import JobSubmissionClient + + runtime_env: dict[str, Any] = {"working_dir": str(working_dir)} + if env_vars: + runtime_env["env_vars"] = dict(env_vars) + if pip: + runtime_env["pip"] = list(pip) + + client = JobSubmissionClient(dashboard) + submitted = client.submit_job( + entrypoint=entrypoint, + runtime_env=runtime_env, + submission_id=submission_id, + ) + return submitted + + +def tail_job_logs(dashboard: str, job_id: str) -> Iterator[str]: + """Yield stdout/stderr lines from a running job. + + Bridges Ray's async iterator into a sync generator via a daemon thread + + queue. Daemon-thread status means the reader stops when the caller exits + (including on KeyboardInterrupt) without us having to coordinate event + loops across threads. + """ + import asyncio + import queue + import threading + + from ray.job_submission import JobSubmissionClient + + q: queue.Queue = queue.Queue() + sentinel: object = object() + + def _worker() -> None: + async def _run() -> None: + client = JobSubmissionClient(dashboard) + async for line in client.tail_job_logs(job_id): + q.put(line) + + try: + asyncio.run(_run()) + except Exception as exc: # noqa: BLE001 + q.put(f"\n[tail error: {exc}]\n") + finally: + q.put(sentinel) + + threading.Thread(target=_worker, name="nrl-k8s-tail", daemon=True).start() + while True: + item = q.get() + if item is sentinel: + return + yield item + + +def wait_for_job( + dashboard: str, + job_id: str, + *, + timeout_s: int | None = None, + poll_s: int = 10, +) -> str: + """Block until a job reaches a terminal state; return that state.""" + from ray.job_submission import JobStatus, JobSubmissionClient + + client = JobSubmissionClient(dashboard) + deadline = None if timeout_s is None else time.monotonic() + timeout_s + while deadline is None or time.monotonic() < deadline: + status = client.get_job_status(job_id) + if status in (JobStatus.SUCCEEDED, JobStatus.FAILED, JobStatus.STOPPED): + return status.value + time.sleep(poll_s) + raise TimeoutError(f"job {job_id} did not finish in {timeout_s}s") + + +# ============================================================================= +# Internals +# ============================================================================= + + +def _free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + + +def _wait_for_tcp( + host: str, port: int, *, timeout_s: int, proc: subprocess.Popen +) -> None: + deadline = time.monotonic() + timeout_s + while time.monotonic() < deadline: + if proc.poll() is not None: + # Process is gone — the drain thread may have already consumed + # stdout, so we can only surface the bare exit code here. + raise RuntimeError( + f"kubectl port-forward exited early (rc={proc.returncode}); " + "is the RayCluster head service reachable?" + ) + try: + with socket.create_connection((host, port), timeout=1): + return + except OSError: + time.sleep(0.5) + raise TimeoutError( + f"kubectl port-forward didn't open {host}:{port} in {timeout_s}s" + ) + + +__all__ = [ + "DASHBOARD_PORT", + "dashboard_url", + "is_in_cluster", + "kubectl_preflight", + "submit_ray_job", + "tail_job_logs", + "wait_for_job", +] diff --git a/tools/nrl_k8s/src/nrl_k8s/workdir.py b/tools/nrl_k8s/src/nrl_k8s/workdir.py new file mode 100644 index 00000000000..08dca5bc52f --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/workdir.py @@ -0,0 +1,96 @@ +"""Stage a working directory for Ray Job ``runtime_env.working_dir`` upload. + +Ray's client-side packager honours ``.gitignore``, which silently drops +training jsonls under ``resources_servers/*/data/`` — we strip those files +from the staged copy so Ray uploads the data. We also skip caches/venvs/git +to keep the zip under Ray's 100 MiB dashboard cap. + +Each call stages into a fresh tmpdir; the caller owns cleanup (Ray uploads +to GCS before the SDK call returns, so deletion is safe afterwards). +""" + +from __future__ import annotations + +import shutil +import tempfile +from pathlib import Path + +_IGNORE_PATTERNS = shutil.ignore_patterns( + ".gitignore", + "__pycache__", + "*.pyc", + "*.pyo", + ".venv", + ".git", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + "*.egg-info", +) + + +DEFAULT_RAY_UPLOAD_PATHS = [ + "nemo_rl", + "examples", + "infra/examples", + "tests/check_metrics.py", + "tests/json_dump_tb_logs.py", + "3rdparty/Gym-workspace/Gym/nemo_gym", + "3rdparty/Gym-workspace/Gym/resources_servers", + "3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model", + "3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent", + "3rdparty/Gym-workspace/Gym/pyproject.toml", + "3rdparty/Gym-workspace/Gym/uv.lock", + "3rdparty/Megatron-LM-workspace/Megatron-LM/megatron", +] + + +# ============================================================================= +# Public API +# ============================================================================= + + +def stage_workdir( + repo_root: Path, + *, + include_paths: list[str] | None = None, + extra_files: dict[str, str] | None = None, +) -> Path: + """Copy the requested subtrees of ``repo_root`` into a fresh tmpdir. + + Args: + repo_root: absolute path to the NeMo-RL repo root. + include_paths: subset of paths (relative to ``repo_root``) to stage. + Defaults to :data:`DEFAULT_RAY_UPLOAD_PATHS`. + extra_files: ``{relative_path: content}`` — additional files to + create in the staged tree (e.g. the merged recipe YAML). + + Returns: + Absolute path to the staged working_dir. + """ + if include_paths is None: + include_paths = DEFAULT_RAY_UPLOAD_PATHS + + dest = Path(tempfile.mkdtemp(prefix="nrl-k8s-workdir-")) + + for rel in include_paths: + src = (repo_root / rel).resolve() + if not src.exists(): + # Missing optional paths are OK (e.g. uv.lock may not exist). + continue + tgt = dest / rel + tgt.parent.mkdir(parents=True, exist_ok=True) + if src.is_dir(): + shutil.copytree(src, tgt, ignore=_IGNORE_PATTERNS) + else: + shutil.copy2(src, tgt) + + for rel, content in (extra_files or {}).items(): + p = dest / rel + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(content) + + return dest + + +__all__ = ["DEFAULT_RAY_UPLOAD_PATHS", "stage_workdir"] diff --git a/tools/nrl_k8s/tests/__init__.py b/tools/nrl_k8s/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tools/nrl_k8s/tests/unit/__init__.py b/tools/nrl_k8s/tests/unit/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tools/nrl_k8s/tests/unit/test_cli.py b/tools/nrl_k8s/tests/unit/test_cli.py new file mode 100644 index 00000000000..a47058b14fb --- /dev/null +++ b/tools/nrl_k8s/tests/unit/test_cli.py @@ -0,0 +1,176 @@ +"""Tests for :mod:`nrl_k8s.cli` — click entrypoints. + +Use ``click.testing.CliRunner`` to invoke commands; every downstream +orchestrate / k8s call is mocked so tests never touch a cluster. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +import yaml +from click.testing import CliRunner +from nrl_k8s import cli +from nrl_k8s import config as cfg_mod + +# ============================================================================= +# Fixtures +# ============================================================================= + + +@pytest.fixture(autouse=True) +def _no_user_defaults(monkeypatch, tmp_path): + """Don't let a real ``~/.config/nrl-k8s/defaults.yaml`` bleed in.""" + monkeypatch.setattr(cfg_mod, "_USER_DEFAULTS", tmp_path / "none.yaml") + + +@pytest.fixture(autouse=True) +def _force_fallback_loader(monkeypatch): + """Force the OmegaConf-only recipe loader (no nemo_rl dependency).""" + import builtins + + real_import = builtins.__import__ + + def _fail_nemo_rl(name, *args, **kwargs): + if name.startswith("nemo_rl"): + raise ImportError("forced-fallback") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", _fail_nemo_rl) + + +def _write_recipe(tmp_path: Path, body: dict) -> Path: + p = tmp_path / "recipe.yaml" + p.write_text(yaml.safe_dump(body)) + return p + + +# ============================================================================= +# check — merged validate + plan +# ============================================================================= + + +class TestCheck: + def test_summary_shows_namespace_and_image(self, tmp_path) -> None: + recipe = _write_recipe( + tmp_path, {"infra": {"namespace": "ns-a", "image": "img:1"}} + ) + runner = CliRunner() + result = runner.invoke(cli.main, ["check", str(recipe)]) + assert result.exit_code == 0, result.output + assert "namespace:" in result.output + assert "ns-a" in result.output + assert "img:1" in result.output + + def test_summary_lists_each_declared_cluster(self, tmp_path) -> None: + spec = { + "headGroupSpec": { + "template": { + "spec": { + "containers": [ + { + "name": "h", + "image": "old", + "resources": {"limits": {"cpu": "8", "memory": "32Gi"}}, + } + ] + } + } + } + } + recipe = _write_recipe( + tmp_path, + { + "infra": { + "namespace": "ns-a", + "image": "img:new", + "clusters": {"training": {"name": "rc-t", "spec": spec}}, + } + }, + ) + runner = CliRunner() + result = runner.invoke(cli.main, ["check", str(recipe)]) + assert result.exit_code == 0, result.output + assert "training: rc-t" in result.output + assert "cpu=8" in result.output + + def test_output_writes_full_config_and_manifests(self, tmp_path) -> None: + spec = { + "headGroupSpec": { + "template": {"spec": {"containers": [{"name": "h", "image": "old"}]}} + } + } + recipe = _write_recipe( + tmp_path, + { + "infra": { + "namespace": "ns-a", + "image": "img:new", + "clusters": {"training": {"name": "rc-t", "spec": spec}}, + } + }, + ) + out = tmp_path / "bundle.json" + runner = CliRunner() + result = runner.invoke(cli.main, ["check", str(recipe), "-o", str(out)]) + assert result.exit_code == 0, result.output + parsed = json.loads(out.read_text()) + assert parsed["infra"]["image"] == "img:new" + assert parsed["manifests"]["training"]["metadata"]["name"] == "rc-t" + # Image is patched through into the rendered manifest. + containers = parsed["manifests"]["training"]["spec"]["headGroupSpec"][ + "template" + ]["spec"]["containers"] + assert containers[0]["image"] == "img:new" + + def test_reports_validation_error_cleanly(self, tmp_path) -> None: + """Missing a required field surfaces as a user-facing ``error:`` line, + not a Python traceback, and exits non-zero. ``image`` is the only + truly-required string — ``namespace`` auto-fills from the kube + context if omitted, so we trigger validation by omitting ``image``. + """ + recipe = _write_recipe(tmp_path, {"infra": {"namespace": "ns-a"}}) + runner = CliRunner() + result = runner.invoke(cli.main, ["check", str(recipe)]) + assert result.exit_code == 1 + assert "error:" in result.output + + +# ============================================================================= +# --infra combined with recipe infra: block +# ============================================================================= + + +class TestInfraCliOption: + def test_both_sources_rejected(self, tmp_path) -> None: + """Passing ``--infra infra.yaml`` while the recipe also has ``infra:`` + errors out instead of silently preferring one. + """ + infra = tmp_path / "infra.yaml" + infra.write_text(yaml.safe_dump({"namespace": "ns-file", "image": "img:file"})) + + recipe = _write_recipe( + tmp_path, {"infra": {"namespace": "ns-inline", "image": "img:inline"}} + ) + runner = CliRunner() + result = runner.invoke(cli.main, ["check", str(recipe), "--infra", str(infra)]) + assert result.exit_code == 1 + assert "infra" in result.output + + +# ============================================================================= +# cluster down +# ============================================================================= + + +class TestClusterDown: + def test_errors_without_role_or_name(self, tmp_path, monkeypatch) -> None: + recipe = _write_recipe( + tmp_path, {"infra": {"namespace": "ns-a", "image": "img:1"}} + ) + runner = CliRunner() + result = runner.invoke(cli.main, ["cluster", "down", str(recipe)]) + assert result.exit_code == 2 + assert "--role" in result.output or "--name" in result.output diff --git a/tools/nrl_k8s/tests/unit/test_config.py b/tools/nrl_k8s/tests/unit/test_config.py new file mode 100644 index 00000000000..0073bda4356 --- /dev/null +++ b/tools/nrl_k8s/tests/unit/test_config.py @@ -0,0 +1,217 @@ +"""Tests for :mod:`nrl_k8s.config` — layered loading of recipe + infra:. + +Priority (low → high): + + 1. Shipped defaults (``nrl_k8s/defaults/defaults.example.yaml``) + 2. User defaults (``$NRL_K8S_DEFAULTS`` or ``~/.config/nrl-k8s/defaults.yaml``) + 3. Recipe ``infra:`` (top-level key on the recipe YAML) + 4. CLI overrides (Hydra-style ``key=value`` list) + +Each test pins one boundary of that precedence rule so regressions are caught +with a single-line diff. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +import yaml +from nrl_k8s import config as cfg_mod +from nrl_k8s.config import load_recipe_with_infra +from nrl_k8s.schema import InfraConfig, SchedulerKind, WorkspaceKind + +# ============================================================================= +# Fixtures +# ============================================================================= + + +@pytest.fixture(autouse=True) +def _no_user_defaults(monkeypatch, tmp_path): + """Point ``_USER_DEFAULTS`` at a non-existent file so user-level config + never bleeds into tests from whatever laptop this runs on. + """ + monkeypatch.setattr(cfg_mod, "_USER_DEFAULTS", tmp_path / "no-such.yaml") + + +@pytest.fixture(autouse=True) +def _force_fallback_loader(monkeypatch): + """Force the OmegaConf-only recipe loader so tests don't depend on nemo_rl. + + The production code prefers nemo_rl's loader (to resolve ``defaults:`` and + ``${mul:...}``) when available; here we want a hermetic fallback. + """ + import builtins + + real_import = builtins.__import__ + + def _fail_nemo_rl(name, *args, **kwargs): # noqa: ANN001 + if name.startswith("nemo_rl"): + raise ImportError("forced-fallback: nemo_rl disabled for this test") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", _fail_nemo_rl) + + +def _write_yaml(path: Path, data: dict) -> Path: + path.write_text(yaml.safe_dump(data)) + return path + + +# ============================================================================= +# Layered precedence +# ============================================================================= + + +class TestPrecedence: + def test_shipped_defaults_alone_plus_required_fields(self, tmp_path) -> None: + """Minimum recipe (just the required namespace + image) validates.""" + recipe = _write_yaml( + tmp_path / "recipe.yaml", + {"infra": {"namespace": "ns-a", "image": "img:1"}}, + ) + loaded = load_recipe_with_infra(recipe) + assert loaded.infra.namespace == "ns-a" + assert loaded.infra.image == "img:1" + # Shipped default: + assert loaded.infra.scheduler.kind is SchedulerKind.DEFAULT + assert loaded.infra.workspace.kind is WorkspaceKind.RAY_UPLOAD + + def test_recipe_overrides_shipped_defaults(self, tmp_path) -> None: + recipe = _write_yaml( + tmp_path / "recipe.yaml", + { + "infra": { + "namespace": "ns", + "image": "img:1", + "scheduler": {"kind": "kai", "queue": "team-a"}, + } + }, + ) + loaded = load_recipe_with_infra(recipe) + assert loaded.infra.scheduler.kind is SchedulerKind.KAI + assert loaded.infra.scheduler.queue == "team-a" + + def test_user_defaults_beat_shipped(self, tmp_path, monkeypatch) -> None: + user = _write_yaml( + tmp_path / "user.yaml", + {"infra": {"scheduler": {"kind": "kai", "queue": "user-q"}}}, + ) + monkeypatch.setattr(cfg_mod, "_USER_DEFAULTS", user) + + recipe = _write_yaml( + tmp_path / "recipe.yaml", + {"infra": {"namespace": "ns", "image": "img:1"}}, + ) + loaded = load_recipe_with_infra(recipe) + assert loaded.infra.scheduler.kind is SchedulerKind.KAI + assert loaded.infra.scheduler.queue == "user-q" + + def test_recipe_beats_user_defaults(self, tmp_path, monkeypatch) -> None: + user = _write_yaml( + tmp_path / "user.yaml", + {"infra": {"scheduler": {"kind": "kai", "queue": "user-q"}}}, + ) + monkeypatch.setattr(cfg_mod, "_USER_DEFAULTS", user) + + recipe = _write_yaml( + tmp_path / "recipe.yaml", + { + "infra": { + "namespace": "ns", + "image": "img:1", + "scheduler": {"kind": "kai", "queue": "recipe-q"}, + } + }, + ) + loaded = load_recipe_with_infra(recipe) + assert loaded.infra.scheduler.queue == "recipe-q" + + def test_cli_overrides_beat_recipe(self, tmp_path) -> None: + recipe = _write_yaml( + tmp_path / "recipe.yaml", + { + "infra": { + "namespace": "ns", + "image": "img:1", + "scheduler": {"kind": "kai", "queue": "recipe-q"}, + } + }, + ) + loaded = load_recipe_with_infra( + recipe, overrides=["infra.scheduler.queue=cli-q"] + ) + assert loaded.infra.scheduler.queue == "cli-q" + + def test_user_defaults_without_infra_wrapper(self, tmp_path, monkeypatch) -> None: + """Users may write either ``{infra: {...}}`` or just the body.""" + user = _write_yaml( + tmp_path / "user.yaml", + {"scheduler": {"kind": "kai", "queue": "bare-q"}}, + ) + monkeypatch.setattr(cfg_mod, "_USER_DEFAULTS", user) + + recipe = _write_yaml( + tmp_path / "recipe.yaml", + {"infra": {"namespace": "ns", "image": "img:1"}}, + ) + loaded = load_recipe_with_infra(recipe) + assert loaded.infra.scheduler.queue == "bare-q" + + +# ============================================================================= +# Recipe handling +# ============================================================================= + + +class TestRecipeBody: + def test_infra_is_peeled_off(self, tmp_path) -> None: + """The returned recipe must not contain `infra:` — the NeMo-RL + entrypoint is never supposed to see it. + """ + recipe = _write_yaml( + tmp_path / "recipe.yaml", + { + "infra": {"namespace": "ns", "image": "img:1"}, + "policy": {"train_micro_batch_size": 2}, + "grpo": {"num_generations_per_prompt": 4}, + }, + ) + loaded = load_recipe_with_infra(recipe) + assert "infra" not in loaded.recipe + assert loaded.recipe["policy"]["train_micro_batch_size"] == 2 + + def test_recipe_without_infra_still_loads(self, tmp_path, monkeypatch) -> None: + """User defaults should supply namespace+image when the recipe omits infra:.""" + user = _write_yaml( + tmp_path / "user.yaml", + {"infra": {"namespace": "ns", "image": "img:1"}}, + ) + monkeypatch.setattr(cfg_mod, "_USER_DEFAULTS", user) + + recipe = _write_yaml( + tmp_path / "recipe.yaml", {"policy": {"train_micro_batch_size": 2}} + ) + loaded = load_recipe_with_infra(recipe) + assert loaded.infra.namespace == "ns" + + def test_recipe_with_non_mapping_infra_rejected(self, tmp_path) -> None: + recipe = _write_yaml(tmp_path / "recipe.yaml", {"infra": ["not", "a", "dict"]}) + with pytest.raises(ValueError): + load_recipe_with_infra(recipe) + + +# ============================================================================= +# Return value shape +# ============================================================================= + + +class TestLoadedConfig: + def test_returns_validated_infra(self, tmp_path) -> None: + recipe = _write_yaml( + tmp_path / "recipe.yaml", + {"infra": {"namespace": "ns", "image": "img:1"}}, + ) + loaded = load_recipe_with_infra(recipe) + assert isinstance(loaded.infra, InfraConfig) + assert loaded.source_path == recipe.resolve() diff --git a/tools/nrl_k8s/tests/unit/test_inspect.py b/tools/nrl_k8s/tests/unit/test_inspect.py new file mode 100644 index 00000000000..638c524aa53 --- /dev/null +++ b/tools/nrl_k8s/tests/unit/test_inspect.py @@ -0,0 +1,142 @@ +"""Tests for :mod:`nrl_k8s.inspect` — read-only introspection of clusters.""" + +from __future__ import annotations + +import contextlib +from types import SimpleNamespace +from unittest.mock import MagicMock + +import pytest +from nrl_k8s import inspect as ins + +# ============================================================================= +# list_cluster_pods — head vs worker by ``ray.io/node-type`` label +# ============================================================================= + + +def _fake_pod(name: str, node_type: str, phase: str = "Running"): + """Build an object shaped like a kubernetes V1Pod.""" + return SimpleNamespace( + metadata=SimpleNamespace(name=name, labels={"ray.io/node-type": node_type}), + status=SimpleNamespace(phase=phase), + ) + + +@pytest.fixture +def mock_core(monkeypatch): + """Stub out the CoreV1Api so no API calls happen.""" + monkeypatch.setattr(ins.k8s, "load_kubeconfig", lambda: None) + fake = MagicMock() + monkeypatch.setattr(ins.client, "CoreV1Api", lambda: fake) + return fake + + +class TestListClusterPods: + def test_splits_head_and_workers(self, mock_core) -> None: + pods_resp = SimpleNamespace( + items=[ + _fake_pod("rc-a-head-0", "head"), + _fake_pod("rc-a-wg-0", "worker"), + _fake_pod("rc-a-wg-1", "worker", phase="Pending"), + ] + ) + mock_core.list_namespaced_pod.return_value = pods_resp + + result = ins.list_cluster_pods("rc-a", "ns-a") + + assert result.head_name == "rc-a-head-0" + assert result.head_phase == "Running" + assert result.worker_names == ["rc-a-wg-0", "rc-a-wg-1"] + assert result.worker_phases == ["Running", "Pending"] + + def test_no_pods_returns_defaults(self, mock_core) -> None: + mock_core.list_namespaced_pod.return_value = SimpleNamespace(items=[]) + result = ins.list_cluster_pods("rc-a", "ns-a") + assert result.head_name is None + assert result.head_phase is None + assert result.worker_names == [] + assert result.worker_phases == [] + + def test_uses_correct_label_selector(self, mock_core) -> None: + mock_core.list_namespaced_pod.return_value = SimpleNamespace(items=[]) + ins.list_cluster_pods("my-rc", "my-ns") + mock_core.list_namespaced_pod.assert_called_once_with( + namespace="my-ns", label_selector="ray.io/cluster=my-rc" + ) + + +# ============================================================================= +# _latest_daemon_job — base id + suffixed matches +# ============================================================================= + + +def _fake_job(submission_id: str, start_time: int, status_value: str): + """Shape of a ``ray.dashboard.modules.job.common.JobDetails``-ish object.""" + return SimpleNamespace( + submission_id=submission_id, + start_time=start_time, + status=SimpleNamespace(value=status_value), + ) + + +def _patch_dashboard(monkeypatch): + @contextlib.contextmanager + def _fake(cluster_name, namespace, **kw): + yield f"http://{cluster_name}.test:8265" + + monkeypatch.setattr(ins.submit, "dashboard_url", _fake) + + +class TestLatestDaemonJob: + def test_prefers_most_recent_suffixed(self, monkeypatch) -> None: + """When both the base id and a suffixed variant exist, pick the + most recent by start_time. + """ + _patch_dashboard(monkeypatch) + + jobs = [ + _fake_job("gym-daemon", start_time=1000, status_value="FAILED"), + _fake_job("gym-daemon-123", start_time=5000, status_value="RUNNING"), + _fake_job("unrelated", start_time=9999, status_value="RUNNING"), + ] + client = MagicMock() + client.list_jobs.return_value = jobs + + import ray.job_submission as rjs + + monkeypatch.setattr(rjs, "JobSubmissionClient", lambda _url: client) + + sid, status = ins._latest_daemon_job("rc-gym", "ns-a", "gym-daemon") + assert sid == "gym-daemon-123" + assert status == "RUNNING" + + def test_falls_back_to_base_id_when_no_match(self, monkeypatch) -> None: + _patch_dashboard(monkeypatch) + + client = MagicMock() + client.list_jobs.return_value = [ + _fake_job("someone-else", 1000, "RUNNING"), + ] + import ray.job_submission as rjs + + monkeypatch.setattr(rjs, "JobSubmissionClient", lambda _url: client) + + sid, status = ins._latest_daemon_job("rc-gym", "ns-a", "gym-daemon") + assert sid == "gym-daemon" + assert status is None + + def test_any_error_returns_base_and_none(self, monkeypatch) -> None: + """Dashboard failures must not crash ``status`` — we fall back to a + 'unknown' row so the CLI can keep rendering the other clusters. + """ + + @contextlib.contextmanager + def _fail(*_a, **_kw): + raise RuntimeError("no kubectl") + yield # unreachable; keeps generator signature valid + + monkeypatch.setattr(ins.submit, "dashboard_url", _fail) + + sid, status = ins._latest_daemon_job("rc-gym", "ns-a", "gym-daemon") + assert sid == "gym-daemon" + assert status is None diff --git a/tools/nrl_k8s/tests/unit/test_k8s.py b/tools/nrl_k8s/tests/unit/test_k8s.py new file mode 100644 index 00000000000..988a8532297 --- /dev/null +++ b/tools/nrl_k8s/tests/unit/test_k8s.py @@ -0,0 +1,192 @@ +"""Tests for :mod:`nrl_k8s.k8s` — thin wrapper around the official k8s client. + +All tests mock ``kubernetes.client`` and ``kubernetes.config`` so they never +touch a live cluster (no kubeconfig read, no HTTP). We also reset the +``lru_cache`` on :func:`load_kubeconfig` between tests so the in-cluster vs +kubeconfig branch can be exercised independently. +""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +import pytest +from kubernetes.client.exceptions import ApiException +from nrl_k8s import k8s + +# ============================================================================= +# Fixtures +# ============================================================================= + + +@pytest.fixture(autouse=True) +def _reset_load_kubeconfig_cache(): + """Drop the @functools.cache memoisation between tests.""" + k8s.load_kubeconfig.cache_clear() + yield + k8s.load_kubeconfig.cache_clear() + + +@pytest.fixture(autouse=True) +def _fast_retry_backoff(monkeypatch): + """Collapse tenacity's backoff so transient-failure tests run fast.""" + monkeypatch.setattr("tenacity.nap.time.sleep", lambda _s: None) + + +@pytest.fixture(autouse=True) +def _no_real_kubeconfig(monkeypatch): + """Stub the config loaders so no test reads a real kubeconfig or /var/run.""" + monkeypatch.setattr(k8s.config, "load_incluster_config", lambda: None) + monkeypatch.setattr(k8s.config, "load_kube_config", lambda: None) + + +@pytest.fixture +def mock_custom_api(monkeypatch): + """Stub ``custom_objects_api()`` to return a MagicMock.""" + api = MagicMock() + monkeypatch.setattr(k8s, "custom_objects_api", lambda: api) + return api + + +def _api_exc(status: int) -> ApiException: + exc = ApiException(status=status) + return exc + + +# ============================================================================= +# load_kubeconfig — in-cluster vs kubeconfig fallback +# ============================================================================= + + +class TestLoadKubeconfig: + def test_uses_incluster_when_available(self, monkeypatch) -> None: + incluster = MagicMock() + kubeconfig = MagicMock() + monkeypatch.setattr(k8s.config, "load_incluster_config", incluster) + monkeypatch.setattr(k8s.config, "load_kube_config", kubeconfig) + + k8s.load_kubeconfig() + + incluster.assert_called_once_with() + kubeconfig.assert_not_called() + + def test_falls_back_to_kubeconfig(self, monkeypatch) -> None: + def _fail_incluster() -> None: + raise k8s.config.ConfigException("no service account") + + kubeconfig = MagicMock() + monkeypatch.setattr(k8s.config, "load_incluster_config", _fail_incluster) + monkeypatch.setattr(k8s.config, "load_kube_config", kubeconfig) + + k8s.load_kubeconfig() + + kubeconfig.assert_called_once_with() + + +# ============================================================================= +# apply_raycluster +# ============================================================================= + + +class TestApplyRaycluster: + _manifest = {"metadata": {"name": "rc-a"}, "spec": {}} + + def test_posts_on_first_call(self, mock_custom_api) -> None: + mock_custom_api.create_namespaced_custom_object.return_value = {"ok": True} + got = k8s.apply_raycluster(self._manifest, "ns-a") + assert got == {"ok": True} + mock_custom_api.create_namespaced_custom_object.assert_called_once() + mock_custom_api.patch_namespaced_custom_object.assert_not_called() + + def test_patches_on_409_conflict(self, mock_custom_api) -> None: + mock_custom_api.create_namespaced_custom_object.side_effect = _api_exc(409) + mock_custom_api.patch_namespaced_custom_object.return_value = {"patched": True} + got = k8s.apply_raycluster(self._manifest, "ns-a") + assert got == {"patched": True} + mock_custom_api.patch_namespaced_custom_object.assert_called_once() + + def test_non_409_bubbles_up(self, mock_custom_api) -> None: + mock_custom_api.create_namespaced_custom_object.side_effect = _api_exc(500) + with pytest.raises(ApiException): + k8s.apply_raycluster(self._manifest, "ns-a") + + +# ============================================================================= +# delete_raycluster +# ============================================================================= + + +class TestDeleteRaycluster: + def test_swallows_404_when_ignore_missing(self, mock_custom_api) -> None: + mock_custom_api.delete_namespaced_custom_object.side_effect = _api_exc(404) + # Should not raise. + k8s.delete_raycluster("rc-gone", "ns-a", ignore_missing=True) + + def test_raises_404_when_not_ignoring(self, mock_custom_api) -> None: + mock_custom_api.delete_namespaced_custom_object.side_effect = _api_exc(404) + with pytest.raises(ApiException): + k8s.delete_raycluster("rc-gone", "ns-a", ignore_missing=False) + + def test_non_404_always_raises(self, mock_custom_api) -> None: + mock_custom_api.delete_namespaced_custom_object.side_effect = _api_exc(500) + with pytest.raises(ApiException): + k8s.delete_raycluster("rc", "ns-a", ignore_missing=True) + + +# ============================================================================= +# wait_for_raycluster_ready +# ============================================================================= + + +class TestWaitForReady: + def test_returns_when_state_ready(self, mock_custom_api, monkeypatch) -> None: + mock_custom_api.get_namespaced_custom_object.return_value = { + "status": {"state": "ready"} + } + # Suppress sleep so the test is fast. + monkeypatch.setattr(k8s.time, "sleep", lambda _s: None) + k8s.wait_for_raycluster_ready("rc-a", "ns-a", timeout_s=5, poll_s=0) + + def test_raises_on_timeout(self, mock_custom_api, monkeypatch) -> None: + mock_custom_api.get_namespaced_custom_object.return_value = { + "status": {"state": "provisioning"} + } + monkeypatch.setattr(k8s.time, "sleep", lambda _s: None) + # Patch only the k8s module's ``time.monotonic`` — tenacity calls + # ``time.monotonic`` through its own ``stop`` helpers and we don't + # want to interfere. Give the wait loop "now > deadline" on the 2nd + # tick so it enters once (to prove the poll happens) then exits. + ticks = iter([0.0, 100.0, 100.0, 100.0]) + monkeypatch.setattr(k8s.time, "monotonic", lambda: next(ticks, 100.0)) + with pytest.raises(TimeoutError): + k8s.wait_for_raycluster_ready("rc-a", "ns-a", timeout_s=10, poll_s=0) + + +# ============================================================================= +# delete_configmap +# ============================================================================= + + +class TestDeleteConfigmap: + def test_returns_true_on_success(self, monkeypatch) -> None: + # Bypass the load_kubeconfig() call inside delete_configmap. + fake_core = MagicMock() + fake_core.delete_namespaced_config_map.return_value = {"ok": True} + monkeypatch.setattr(k8s.client, "CoreV1Api", lambda: fake_core) + + assert k8s.delete_configmap("cm", "ns") is True + + def test_returns_false_on_404_when_ignoring(self, monkeypatch) -> None: + fake_core = MagicMock() + fake_core.delete_namespaced_config_map.side_effect = _api_exc(404) + monkeypatch.setattr(k8s.client, "CoreV1Api", lambda: fake_core) + + assert k8s.delete_configmap("cm", "ns", ignore_missing=True) is False + + def test_raises_on_404_when_not_ignoring(self, monkeypatch) -> None: + fake_core = MagicMock() + fake_core.delete_namespaced_config_map.side_effect = _api_exc(404) + monkeypatch.setattr(k8s.client, "CoreV1Api", lambda: fake_core) + + with pytest.raises(ApiException): + k8s.delete_configmap("cm", "ns", ignore_missing=False) diff --git a/tools/nrl_k8s/tests/unit/test_manifest.py b/tools/nrl_k8s/tests/unit/test_manifest.py new file mode 100644 index 00000000000..ae40deccd6b --- /dev/null +++ b/tools/nrl_k8s/tests/unit/test_manifest.py @@ -0,0 +1,201 @@ +"""Tests for :mod:`nrl_k8s.manifest` — RayCluster manifest builder.""" + +from __future__ import annotations + +from nrl_k8s.manifest import build_raycluster_manifest +from nrl_k8s.schema import ClusterSpec, InfraConfig + +# ============================================================================= +# Fixtures +# ============================================================================= + + +def _base_spec() -> dict: + return { + "headGroupSpec": { + "template": { + "spec": { + "containers": [{"name": "ray-head", "image": "registry/img:old"}], + } + } + }, + "workerGroupSpecs": [ + { + "groupName": "gpu-workers", + "template": { + "spec": { + "containers": [ + {"name": "ray-worker", "image": "registry/img:old"} + ], + } + }, + } + ], + } + + +def _make_infra(**overrides) -> InfraConfig: + payload = {"namespace": "ns-patched", "image": "registry/img:new"} | overrides + return InfraConfig.model_validate(payload) + + +def _make_cluster(**overrides) -> ClusterSpec: + payload = {"name": "rc-test", "spec": _base_spec()} | overrides + return ClusterSpec.model_validate(payload) + + +# ============================================================================= +# Envelope + metadata +# ============================================================================= + + +class TestEnvelope: + def test_apiversion_and_kind(self) -> None: + got = build_raycluster_manifest(_make_cluster(), _make_infra()) + assert got["apiVersion"] == "ray.io/v1" + assert got["kind"] == "RayCluster" + + def test_metadata_name_and_namespace(self) -> None: + got = build_raycluster_manifest(_make_cluster(name="rc-x"), _make_infra()) + assert got["metadata"]["name"] == "rc-x" + assert got["metadata"]["namespace"] == "ns-patched" + + def test_labels_merged_from_infra_and_cluster(self) -> None: + cluster = _make_cluster(labels={"role": "training"}) + infra = _make_infra(labels={"team": "rl"}) + got = build_raycluster_manifest(cluster, infra) + assert got["metadata"]["labels"] == {"role": "training", "team": "rl"} + + def test_cluster_labels_win_on_collision(self) -> None: + cluster = _make_cluster(labels={"team": "override"}) + infra = _make_infra(labels={"team": "rl"}) + got = build_raycluster_manifest(cluster, infra) + assert got["metadata"]["labels"] == {"team": "override"} + + def test_no_labels_key_when_empty(self) -> None: + got = build_raycluster_manifest(_make_cluster(), _make_infra()) + assert "labels" not in got["metadata"] + + +# ============================================================================= +# Cross-cutting patches +# ============================================================================= + + +class TestPatching: + def test_image_propagates_to_every_container(self) -> None: + got = build_raycluster_manifest(_make_cluster(), _make_infra()) + head = got["spec"]["headGroupSpec"]["template"]["spec"]["containers"][0] + worker = got["spec"]["workerGroupSpecs"][0]["template"]["spec"]["containers"][0] + assert head["image"] == "registry/img:new" + assert worker["image"] == "registry/img:new" + + def test_image_pull_secrets_applied(self) -> None: + infra = _make_infra(imagePullSecrets=["a", "b"]) + got = build_raycluster_manifest(_make_cluster(), infra) + for pod in ( + got["spec"]["headGroupSpec"]["template"]["spec"], + got["spec"]["workerGroupSpecs"][0]["template"]["spec"], + ): + assert pod["imagePullSecrets"] == [{"name": "a"}, {"name": "b"}] + + def test_image_pull_secrets_skipped_when_empty(self) -> None: + spec = _base_spec() + spec["headGroupSpec"]["template"]["spec"]["imagePullSecrets"] = [ + {"name": "pre"} + ] + got = build_raycluster_manifest(_make_cluster(spec=spec), _make_infra()) + head = got["spec"]["headGroupSpec"]["template"]["spec"] + assert head["imagePullSecrets"] == [{"name": "pre"}] + + def test_service_account_patched_when_set(self) -> None: + infra = _make_infra(serviceAccount="new-sa") + got = build_raycluster_manifest(_make_cluster(), infra) + head = got["spec"]["headGroupSpec"]["template"]["spec"] + assert head["serviceAccountName"] == "new-sa" + + def test_service_account_untouched_when_unset(self) -> None: + spec = _base_spec() + spec["headGroupSpec"]["template"]["spec"]["serviceAccountName"] = "original" + got = build_raycluster_manifest(_make_cluster(spec=spec), _make_infra()) + head = got["spec"]["headGroupSpec"]["template"]["spec"] + assert head["serviceAccountName"] == "original" + + +# ============================================================================= +# Immutability +# ============================================================================= + + +class TestImmutability: + def test_input_spec_not_mutated(self) -> None: + """Patching writes into a deep copy so the recipe-parsed ClusterSpec + isn't mutated between calls (important when the same spec underlies + multiple renders). + """ + spec = _base_spec() + original = spec["headGroupSpec"]["template"]["spec"]["containers"][0]["image"] + cluster = _make_cluster(spec=spec) + build_raycluster_manifest(cluster, _make_infra()) + assert ( + spec["headGroupSpec"]["template"]["spec"]["containers"][0]["image"] + == original + ) + + def test_two_builds_see_same_input(self) -> None: + """Two successive build calls against the same ClusterSpec produce + identical manifests — i.e. the first call didn't leave any mutation + behind on the ClusterSpec's inner ``spec`` dict. + """ + cluster = _make_cluster() + infra = _make_infra() + first = build_raycluster_manifest(cluster, infra) + second = build_raycluster_manifest(cluster, infra) + assert first == second + + +# ============================================================================= +# Labels + annotations: merge precedence (cluster wins over infra) +# ============================================================================= + + +class TestLabelsAnnotationsMerge: + def test_annotations_merged_from_infra_and_cluster(self) -> None: + cluster = _make_cluster(annotations={"kyverno.io/skip": "true"}) + infra = _make_infra(annotations={"platform/team": "rl"}) + got = build_raycluster_manifest(cluster, infra) + assert got["metadata"]["annotations"] == { + "kyverno.io/skip": "true", + "platform/team": "rl", + } + + def test_cluster_annotations_win_on_collision(self) -> None: + cluster = _make_cluster(annotations={"team": "override"}) + infra = _make_infra(annotations={"team": "infra"}) + got = build_raycluster_manifest(cluster, infra) + assert got["metadata"]["annotations"] == {"team": "override"} + + def test_no_annotations_key_when_empty(self) -> None: + got = build_raycluster_manifest(_make_cluster(), _make_infra()) + assert "annotations" not in got["metadata"] + + +# ============================================================================= +# ServiceAccount patching onto pods that already set one +# ============================================================================= + + +class TestServiceAccountOverride: + def test_service_account_overrides_existing_on_head_and_worker(self) -> None: + """When ``infra.serviceAccount`` is set it overwrites any existing + ``serviceAccountName`` on every pod template (head + all workers). + """ + spec = _base_spec() + spec["headGroupSpec"]["template"]["spec"]["serviceAccountName"] = "old-head" + spec["workerGroupSpecs"][0]["template"]["spec"]["serviceAccountName"] = "old-wg" + infra = _make_infra(serviceAccount="new-sa") + got = build_raycluster_manifest(_make_cluster(spec=spec), infra) + head = got["spec"]["headGroupSpec"]["template"]["spec"] + worker = got["spec"]["workerGroupSpecs"][0]["template"]["spec"] + assert head["serviceAccountName"] == "new-sa" + assert worker["serviceAccountName"] == "new-sa" diff --git a/tools/nrl_k8s/tests/unit/test_orchestrate.py b/tools/nrl_k8s/tests/unit/test_orchestrate.py new file mode 100644 index 00000000000..c8d5f87bc8b --- /dev/null +++ b/tools/nrl_k8s/tests/unit/test_orchestrate.py @@ -0,0 +1,307 @@ +"""Tests for :mod:`nrl_k8s.orchestrate` — the bring-up / submit pipeline. + +Every external system is stubbed: no Kubernetes API, no Ray dashboard, no +workdir staging. We assert on the *orchestration decisions*: when do we +apply, when do we skip, when do we suffix a submissionId for ``--replace``. +""" + +from __future__ import annotations + +import contextlib +from pathlib import Path +from unittest.mock import MagicMock + +import pytest +from nrl_k8s import orchestrate +from nrl_k8s.config import LoadedConfig +from nrl_k8s.schema import InfraConfig + +# ============================================================================= +# Fake config builders +# ============================================================================= + + +def _infra_payload( + *, + gym_entrypoint: str | None = None, + gym_submission_id: str | None = "gym-daemon", + training_entrypoint: str | None = "python -m train", + gym_health_url: str | None = None, +) -> dict: + """Construct an InfraConfig dict with up to 2 declared clusters.""" + base = { + "namespace": "ns-a", + "image": "img:1", + "launch": {"entrypoint": training_entrypoint}, + "clusters": { + "training": { + "name": "rc-train", + "spec": { + "headGroupSpec": { + "template": {"spec": {"containers": [{"name": "ray-head"}]}} + } + }, + }, + }, + } + if gym_entrypoint is not None: + base["clusters"]["gym"] = { + "name": "rc-gym", + "spec": { + "headGroupSpec": { + "template": {"spec": {"containers": [{"name": "ray-head"}]}} + } + }, + "daemon": { + "entrypoint": gym_entrypoint, + "submissionId": gym_submission_id, + "healthCheckUrl": gym_health_url, + }, + } + return base + + +def _loaded(**kwargs) -> LoadedConfig: + from omegaconf import OmegaConf + + infra = InfraConfig.model_validate(_infra_payload(**kwargs)) + return LoadedConfig( + recipe=OmegaConf.create({"policy": {"x": 1}}), + infra=infra, + source_path=Path("/tmp/recipe.yaml"), + ) + + +@pytest.fixture +def log(): + """Collect log lines in a list; assert on substrings in tests.""" + lines: list[str] = [] + return lines.append, lines + + +# ============================================================================= +# bring_up_cluster +# ============================================================================= + + +class TestBringUpCluster: + def test_applies_and_waits(self, monkeypatch, log) -> None: + """The happy path calls both apply + wait_for_ready.""" + log_fn, lines = log + apply = MagicMock() + wait = MagicMock() + monkeypatch.setattr(orchestrate.k8s, "apply_raycluster", apply) + monkeypatch.setattr(orchestrate.k8s, "wait_for_raycluster_ready", wait) + + loaded = _loaded(gym_entrypoint="python gym.py --job-id run-42") + name = orchestrate.bring_up_cluster("gym", loaded, log=log_fn) + + assert name == "rc-gym" + apply.assert_called_once() + wait.assert_called_once_with("rc-gym", "ns-a", timeout_s=900) + + def test_no_wait_skips_readiness(self, monkeypatch, log) -> None: + log_fn, _ = log + apply = MagicMock() + wait = MagicMock() + monkeypatch.setattr(orchestrate.k8s, "apply_raycluster", apply) + monkeypatch.setattr(orchestrate.k8s, "wait_for_raycluster_ready", wait) + + orchestrate.bring_up_cluster( + "training", _loaded(), log=log_fn, wait_ready=False + ) + apply.assert_called_once() + wait.assert_not_called() + + +# ============================================================================= +# submit_daemon — status-driven branching +# ============================================================================= + + +def _patch_dashboard(monkeypatch): + """Force dashboard_url to yield a dummy URL without any port-forward.""" + + @contextlib.contextmanager + def _fake(cluster_name, namespace, **kw): + yield f"http://{cluster_name}.test:8265" + + monkeypatch.setattr(orchestrate.submit, "dashboard_url", _fake) + + +def _patch_client(monkeypatch, client): + """Install ``client`` as the ``JobSubmissionClient`` constructor's return.""" + monkeypatch.setattr(orchestrate, "JobSubmissionClient", lambda *_a, **_kw: client) + + +class TestSubmitDaemon: + def test_skips_when_existing_running(self, monkeypatch, log) -> None: + """A RUNNING daemon without ``--replace`` returns the existing id.""" + from ray.job_submission import JobStatus + + log_fn, lines = log + loaded = _loaded(gym_entrypoint="python gym.py --job-id run-1") + + _patch_dashboard(monkeypatch) + client = MagicMock() + client.get_job_status.return_value = JobStatus.RUNNING + _patch_client(monkeypatch, client) + + staged = MagicMock() + monkeypatch.setattr(orchestrate.workdir, "stage_workdir", staged) + submit_job = MagicMock() + monkeypatch.setattr(orchestrate.submit, "submit_ray_job", submit_job) + + out = orchestrate.submit_daemon( + "gym", loaded, "rc-gym", log=log_fn, repo_root=Path("/tmp") + ) + + assert out == "gym-daemon" + submit_job.assert_not_called() + staged.assert_not_called() + assert any("already RUNNING" in ln for ln in lines) + + def test_skips_when_existing_succeeded(self, monkeypatch, log) -> None: + from ray.job_submission import JobStatus + + log_fn, lines = log + loaded = _loaded(gym_entrypoint="python gym.py --job-id run-1") + _patch_dashboard(monkeypatch) + client = MagicMock() + client.get_job_status.return_value = JobStatus.SUCCEEDED + _patch_client(monkeypatch, client) + submit_job = MagicMock() + monkeypatch.setattr(orchestrate.submit, "submit_ray_job", submit_job) + + orchestrate.submit_daemon( + "gym", loaded, "rc-gym", log=log_fn, repo_root=Path("/tmp") + ) + submit_job.assert_not_called() + + def test_raises_when_existing_failed_without_replace( + self, monkeypatch, log + ) -> None: + from ray.job_submission import JobStatus + + log_fn, _ = log + loaded = _loaded(gym_entrypoint="python gym.py --job-id run-1") + _patch_dashboard(monkeypatch) + client = MagicMock() + client.get_job_status.return_value = JobStatus.FAILED + _patch_client(monkeypatch, client) + + with pytest.raises(RuntimeError, match="FAILED"): + orchestrate.submit_daemon( + "gym", loaded, "rc-gym", log=log_fn, repo_root=Path("/tmp") + ) + + def test_replace_stops_running_and_suffixes_id(self, monkeypatch, log) -> None: + """``--replace`` on a RUNNING daemon stops it and picks a fresh suffix.""" + from ray.job_submission import JobStatus + + log_fn, lines = log + loaded = _loaded(gym_entrypoint="python gym.py --job-id run-1") + _patch_dashboard(monkeypatch) + client = MagicMock() + # initial status check says RUNNING, then STOPPED once we stop. + client.get_job_status.side_effect = [JobStatus.RUNNING, JobStatus.STOPPED] + _patch_client(monkeypatch, client) + + monkeypatch.setattr( + orchestrate.workdir, "stage_workdir", lambda *a, **kw: Path("/tmp/wd") + ) + submit_job = MagicMock(return_value="gym-daemon-123") + monkeypatch.setattr(orchestrate.submit, "submit_ray_job", submit_job) + # Collapse sleep in _wait_job_stopped. + monkeypatch.setattr(orchestrate.time, "sleep", lambda _s: None) + + out = orchestrate.submit_daemon( + "gym", + loaded, + "rc-gym", + log=log_fn, + repo_root=Path("/tmp"), + replace=True, + ) + + client.stop_job.assert_called_once_with("gym-daemon") + submit_job.assert_called_once() + kwargs = submit_job.call_args.kwargs + # The fresh submission_id must start with the original name + "-". + assert kwargs["submission_id"].startswith("gym-daemon-") + assert kwargs["submission_id"] != "gym-daemon" + assert out == "gym-daemon-123" + + def test_no_daemon_returns_none(self, monkeypatch, log) -> None: + """A cluster without a daemon is a no-op.""" + log_fn, _ = log + loaded = _loaded() # no gym cluster declared + # Give training cluster a fake daemon=None — just call on training. + out = orchestrate.submit_daemon( + "training", loaded, "rc-train", log=log_fn, repo_root=Path("/tmp") + ) + assert out is None + + +# ============================================================================= +# submit_training +# ============================================================================= + + +class TestSubmitTraining: + def test_raises_when_entrypoint_unset(self, monkeypatch, log) -> None: + log_fn, _ = log + loaded = _loaded(training_entrypoint=None) + with pytest.raises(ValueError, match="entrypoint"): + orchestrate.submit_training(loaded, log=log_fn, repo_root=Path("/tmp")) + + +# ============================================================================= +# _infer_disagg_job_id — regex over gym entrypoints +# ============================================================================= + + +class TestInferDisaggJobId: + def test_parses_equals_form(self) -> None: + loaded = _loaded(gym_entrypoint="python gym.py --job-id=run-xyz --other") + assert orchestrate._infer_disagg_job_id(loaded.infra) == "run-xyz" + + def test_parses_space_form(self) -> None: + loaded = _loaded(gym_entrypoint="python gym.py --job-id run-abc --other") + assert orchestrate._infer_disagg_job_id(loaded.infra) == "run-abc" + + def test_returns_none_when_no_flag(self) -> None: + loaded = _loaded(gym_entrypoint="python gym.py --cluster x") + assert orchestrate._infer_disagg_job_id(loaded.infra) is None + + def test_returns_none_when_no_gym(self) -> None: + loaded = _loaded() # no gym cluster + assert orchestrate._infer_disagg_job_id(loaded.infra) is None + + +# ============================================================================= +# _reset_endpoint_registry — deletes ConfigMap named after the inferred id +# ============================================================================= + + +class TestResetEndpointRegistry: + def test_deletes_cm_named_after_job_id(self, monkeypatch, log) -> None: + log_fn, lines = log + loaded = _loaded(gym_entrypoint="python gym.py --job-id=run-k") + + delete_cm = MagicMock(return_value=True) + monkeypatch.setattr(orchestrate.k8s, "delete_configmap", delete_cm) + + orchestrate._reset_endpoint_registry(loaded, log=log_fn) + + delete_cm.assert_called_once_with("nemo-rl-endpoints-run-k", "ns-a") + assert any("deleted endpoint registry" in ln for ln in lines) + + def test_noop_when_no_gym(self, monkeypatch, log) -> None: + log_fn, _ = log + loaded = _loaded() # no gym cluster + delete_cm = MagicMock() + monkeypatch.setattr(orchestrate.k8s, "delete_configmap", delete_cm) + + orchestrate._reset_endpoint_registry(loaded, log=log_fn) + delete_cm.assert_not_called() diff --git a/tools/nrl_k8s/tests/unit/test_schema.py b/tools/nrl_k8s/tests/unit/test_schema.py new file mode 100644 index 00000000000..3d9aa442a19 --- /dev/null +++ b/tools/nrl_k8s/tests/unit/test_schema.py @@ -0,0 +1,215 @@ +"""Tests for :mod:`nrl_k8s.schema`. + +The schema is the contract between recipes and every downstream template, so +these tests pin the exact validation rules. Each cluster-identifying field +must be present; every ``kind=...`` sentinel with a required companion +(``queue``, ``pvcName``, ``hostPath``) must refuse a config that omits the +companion; and ``extra='forbid'`` must reject typos like ``queue_name``. +""" + +from __future__ import annotations + +import pytest +from nrl_k8s.schema import ( + CheckpointsKind, + CheckpointsSpec, + HFCacheKind, + HFCacheSpec, + InfraConfig, + LaunchMode, + LaunchSpec, + SchedulerKind, + SchedulerSpec, + WorkspaceKind, + WorkspaceSpec, +) +from pydantic import ValidationError + +# ============================================================================= +# Top-level InfraConfig +# ============================================================================= + + +def _min_infra() -> dict: + """The smallest valid InfraConfig payload (required fields only).""" + return {"namespace": "nemo-rl", "image": "nvcr.io/nvidia/nemo-rl:test"} + + +class TestInfraConfigRequiredFields: + def test_minimal_config_validates(self) -> None: + cfg = InfraConfig.model_validate(_min_infra()) + assert cfg.namespace == "nemo-rl" + assert cfg.image == "nvcr.io/nvidia/nemo-rl:test" + assert cfg.scheduler.kind is SchedulerKind.DEFAULT + assert cfg.workspace.kind is WorkspaceKind.RAY_UPLOAD + + def test_missing_namespace_is_rejected(self) -> None: + with pytest.raises(ValidationError): + InfraConfig.model_validate({"image": "foo:bar"}) + + def test_missing_image_is_rejected(self) -> None: + with pytest.raises(ValidationError): + InfraConfig.model_validate({"namespace": "ns"}) + + def test_blank_namespace_is_rejected(self) -> None: + with pytest.raises(ValidationError): + InfraConfig.model_validate({"namespace": " ", "image": "foo:bar"}) + + +class TestInfraConfigStrictness: + def test_unknown_top_level_key_rejected(self) -> None: + payload = _min_infra() | {"totally_fake_key": True} + with pytest.raises(ValidationError): + InfraConfig.model_validate(payload) + + def test_unknown_nested_key_rejected(self) -> None: + payload = _min_infra() | {"scheduler": {"kind": "default", "typo": "x"}} + with pytest.raises(ValidationError): + InfraConfig.model_validate(payload) + + +# ============================================================================= +# Scheduler +# ============================================================================= + + +class TestSchedulerSpec: + def test_default_requires_no_queue(self) -> None: + spec = SchedulerSpec() + assert spec.kind is SchedulerKind.DEFAULT + assert spec.queue is None + + def test_kai_without_queue_rejected(self) -> None: + with pytest.raises(ValidationError): + SchedulerSpec.model_validate({"kind": "kai"}) + + def test_kueue_without_queue_rejected(self) -> None: + with pytest.raises(ValidationError): + SchedulerSpec.model_validate({"kind": "kueue"}) + + def test_kai_with_queue_accepted(self) -> None: + spec = SchedulerSpec.model_validate({"kind": "kai", "queue": "team-a"}) + assert spec.queue == "team-a" + + def test_invalid_kind_rejected(self) -> None: + with pytest.raises(ValidationError): + SchedulerSpec.model_validate({"kind": "slurm"}) + + +# ============================================================================= +# Workspace +# ============================================================================= + + +class TestWorkspaceSpec: + def test_default_is_ray_upload(self) -> None: + spec = WorkspaceSpec() + assert spec.kind is WorkspaceKind.RAY_UPLOAD + + @pytest.mark.parametrize("kind", ["lustre", "pvc"]) + def test_pvc_kinds_require_pvc_name(self, kind: str) -> None: + with pytest.raises(ValidationError): + WorkspaceSpec.model_validate({"kind": kind}) + + def test_lustre_with_pvc_name_accepted(self) -> None: + spec = WorkspaceSpec.model_validate( + {"kind": "lustre", "pvcName": "nemo-rl-lustre", "size": "1200Gi"} + ) + assert spec.pvcName == "nemo-rl-lustre" + assert spec.mountPath == "/mnt/nemo-rl" # default preserved + + def test_host_path_without_host_path_rejected(self) -> None: + with pytest.raises(ValidationError): + WorkspaceSpec.model_validate({"kind": "hostPath"}) + + def test_host_path_with_host_path_accepted(self) -> None: + spec = WorkspaceSpec.model_validate({"kind": "hostPath", "hostPath": "/data"}) + assert spec.hostPath == "/data" + + +# ============================================================================= +# HFCache / Checkpoints +# ============================================================================= + + +class TestHFCacheSpec: + def test_default_is_none(self) -> None: + assert HFCacheSpec().kind is HFCacheKind.NONE + + @pytest.mark.parametrize("kind", ["lustre", "pvc"]) + def test_pvc_kinds_require_name(self, kind: str) -> None: + with pytest.raises(ValidationError): + HFCacheSpec.model_validate({"kind": kind}) + + +class TestCheckpointsSpec: + def test_default_is_none(self) -> None: + assert CheckpointsSpec().kind is CheckpointsKind.NONE + + def test_lustre_without_pvc_rejected(self) -> None: + with pytest.raises(ValidationError): + CheckpointsSpec.model_validate({"kind": "lustre"}) + + +# ============================================================================= +# Launch +# ============================================================================= + + +class TestLaunchSpec: + def test_default_is_single(self) -> None: + spec = LaunchSpec() + assert spec.mode is LaunchMode.SINGLE + assert spec.peerWatcher is True + + def test_attach_requires_a_target(self) -> None: + with pytest.raises(ValidationError): + LaunchSpec.model_validate({"mode": "attach", "attach": {}}) + + def test_attach_with_generation_only_ok(self) -> None: + spec = LaunchSpec.model_validate( + {"mode": "attach", "attach": {"generation": "rc-gen"}} + ) + assert spec.attach.generation == "rc-gen" + assert spec.attach.training is None + + +# ============================================================================= +# Placement / tolerations +# ============================================================================= + + +class TestTolerations: + def test_simple_toleration_accepted(self) -> None: + cfg = InfraConfig.model_validate( + _min_infra() + | { + "placement": { + "nodeSelector": {"gpu": "h100"}, + "tolerations": [ + { + "key": "team", + "operator": "Equal", + "value": "rl", + "effect": "NoSchedule", + } + ], + } + } + ) + assert cfg.placement.nodeSelector == {"gpu": "h100"} + assert len(cfg.placement.tolerations) == 1 + assert cfg.placement.tolerations[0].key == "team" + + def test_invalid_effect_rejected(self) -> None: + with pytest.raises(ValidationError): + InfraConfig.model_validate( + _min_infra() + | { + "placement": { + "tolerations": [ + {"key": "k", "effect": "DoesntExist"}, + ] + } + } + ) diff --git a/tools/nrl_k8s/tests/unit/test_submit.py b/tools/nrl_k8s/tests/unit/test_submit.py new file mode 100644 index 00000000000..9c61ec9213d --- /dev/null +++ b/tools/nrl_k8s/tests/unit/test_submit.py @@ -0,0 +1,122 @@ +"""Tests for :mod:`nrl_k8s.submit` — dashboard access + Ray job submission. + +``dashboard_url`` has two branches (in-cluster DNS vs. laptop port-forward) +and both must be exercised without spawning anything real. ``tail_job_logs`` +bridges Ray's async iterator to a sync generator via a daemon thread. +""" + +from __future__ import annotations + +import io +from unittest.mock import MagicMock + +import pytest +from nrl_k8s import submit + +# ============================================================================= +# is_in_cluster +# ============================================================================= + + +class TestIsInCluster: + def test_true_when_env_set(self, monkeypatch) -> None: + monkeypatch.setenv("KUBERNETES_SERVICE_HOST", "10.0.0.1") + assert submit.is_in_cluster() is True + + def test_false_when_env_unset(self, monkeypatch) -> None: + monkeypatch.delenv("KUBERNETES_SERVICE_HOST", raising=False) + assert submit.is_in_cluster() is False + + +# ============================================================================= +# dashboard_url — in-cluster vs laptop (kubectl port-forward) +# ============================================================================= + + +class TestDashboardUrlInCluster: + def test_returns_dns_url_without_spawning(self, monkeypatch) -> None: + """In-cluster path returns the head-svc DNS URL and must not touch + ``subprocess.Popen``. + """ + monkeypatch.setattr(submit, "is_in_cluster", lambda: True) + + def _boom(*a, **kw): # pragma: no cover — should never run + raise AssertionError("Popen must not be called in-cluster") + + monkeypatch.setattr(submit.subprocess, "Popen", _boom) + + with submit.dashboard_url("rc-gen", "ns-a") as url: + assert url == "http://rc-gen-head-svc.ns-a.svc.cluster.local:8265" + + +class TestDashboardUrlLaptop: + def test_spawns_kubectl_and_tears_down(self, monkeypatch) -> None: + """Laptop path spawns ``kubectl port-forward`` and terminates it on exit.""" + monkeypatch.setattr(submit, "is_in_cluster", lambda: False) + # Skip the kubectl preflight (it runs real subprocesses). + monkeypatch.setattr(submit, "_KUBECTL_OK", True) + # Collapse _wait_for_tcp so we never try to open a real socket. + monkeypatch.setattr(submit, "_wait_for_tcp", lambda *a, **kw: None) + monkeypatch.setattr(submit, "_free_port", lambda: 19999) + + proc = MagicMock() + proc.poll.return_value = None # "still running" + proc.stdout = io.StringIO("") + + popen = MagicMock(return_value=proc) + monkeypatch.setattr(submit.subprocess, "Popen", popen) + + with submit.dashboard_url("rc-gen", "ns-a") as url: + assert url == "http://127.0.0.1:19999" + + # Popen was called with kubectl port-forward, and we tore it down. + popen.assert_called_once() + cmd = popen.call_args[0][0] + assert cmd[0] == "kubectl" + assert "port-forward" in cmd + proc.terminate.assert_called_once() + + def test_missing_kubectl_raises(self, monkeypatch) -> None: + monkeypatch.setattr(submit, "is_in_cluster", lambda: False) + monkeypatch.setattr(submit, "_KUBECTL_OK", False) + monkeypatch.setattr(submit.shutil, "which", lambda _bin: None) + with pytest.raises(RuntimeError, match="kubectl not found"): + with submit.dashboard_url("rc-gen", "ns-a"): + pass + + +# ============================================================================= +# tail_job_logs — bridge async -> sync +# ============================================================================= + + +class TestTailJobLogs: + def test_bridges_async_iterator_to_generator(self, monkeypatch) -> None: + """``tail_job_logs`` must yield each line produced by the async Ray + iterator, then stop cleanly when the iterator is exhausted. + """ + fake_lines = ["line one\n", "line two\n", "done\n"] + + class _FakeAsyncIter: + def __init__(self, lines): + self._it = iter(lines) + + def __aiter__(self): + return self + + async def __anext__(self): + try: + return next(self._it) + except StopIteration: + raise StopAsyncIteration + + fake_client = MagicMock() + fake_client.tail_job_logs = lambda _jid: _FakeAsyncIter(fake_lines) + + # Stub the lazy import inside tail_job_logs. + import ray.job_submission as rjs + + monkeypatch.setattr(rjs, "JobSubmissionClient", lambda _url: fake_client) + + out = list(submit.tail_job_logs("http://127.0.0.1:8265", "job-xyz")) + assert out == fake_lines diff --git a/tools/nrl_k8s/tests/unit/test_workdir.py b/tools/nrl_k8s/tests/unit/test_workdir.py new file mode 100644 index 00000000000..5dee673cdb9 --- /dev/null +++ b/tools/nrl_k8s/tests/unit/test_workdir.py @@ -0,0 +1,108 @@ +"""Tests for :mod:`nrl_k8s.workdir` — staging the Ray ``working_dir`` upload. + +These tests build a small fixture repo tree under ``tmp_path`` and verify: + +* ``include_paths`` filters the staged subset. +* Missing optional paths are skipped silently. +* Ignore patterns drop ``.gitignore`` (Ray's packager uses it and silently + loses training JSONLs), ``__pycache__``, and related caches. +* ``extra_files`` lands the merged recipe into the staged root. +""" + +from __future__ import annotations + +import shutil +from pathlib import Path + +import pytest +from nrl_k8s.workdir import stage_workdir + +# ============================================================================= +# Fixtures +# ============================================================================= + + +@pytest.fixture +def fake_repo(tmp_path: Path) -> Path: + """A minimal repo tree with one source dir + a cache dir + a .gitignore.""" + root = tmp_path / "repo" + (root / "nemo_rl").mkdir(parents=True) + (root / "nemo_rl" / "mod.py").write_text("x = 1\n") + (root / "nemo_rl" / ".gitignore").write_text("data.jsonl\n") + (root / "nemo_rl" / "data.jsonl").write_text("{}\n") + (root / "nemo_rl" / "__pycache__").mkdir() + (root / "nemo_rl" / "__pycache__" / "mod.cpython.pyc").write_text("bin") + + (root / "examples").mkdir() + (root / "examples" / "run.py").write_text("print('hi')\n") + + # a single-file include target (like tests/check_metrics.py) + (root / "tests").mkdir() + (root / "tests" / "check_metrics.py").write_text("# metrics\n") + + return root + + +# ============================================================================= +# Tests +# ============================================================================= + + +class TestStageWorkdir: + def test_respects_include_paths(self, fake_repo: Path) -> None: + """Only the requested subtrees end up in the staged dir.""" + dest = stage_workdir(fake_repo, include_paths=["nemo_rl"]) + try: + assert (dest / "nemo_rl" / "mod.py").is_file() + # 'examples' was not requested, must not leak in. + assert not (dest / "examples").exists() + finally: + shutil.rmtree(dest, ignore_errors=True) + + def test_skips_missing_optional_paths(self, fake_repo: Path) -> None: + """A path in ``include_paths`` that doesn't exist is silently skipped.""" + dest = stage_workdir( + fake_repo, + include_paths=["nemo_rl", "3rdparty/Gym-workspace/Gym/uv.lock"], + ) + try: + assert (dest / "nemo_rl" / "mod.py").is_file() + assert not (dest / "3rdparty").exists() + finally: + shutil.rmtree(dest, ignore_errors=True) + + def test_strips_gitignore_and_pycache(self, fake_repo: Path) -> None: + """``.gitignore`` and ``__pycache__`` are scrubbed from the staged copy + because Ray's packager honours ``.gitignore`` and would drop data files. + """ + dest = stage_workdir(fake_repo, include_paths=["nemo_rl"]) + try: + # The source files that would otherwise be dropped must remain. + assert (dest / "nemo_rl" / "data.jsonl").is_file() + # But the ignore-triggering files must be gone. + assert not (dest / "nemo_rl" / ".gitignore").exists() + assert not (dest / "nemo_rl" / "__pycache__").exists() + finally: + shutil.rmtree(dest, ignore_errors=True) + + def test_writes_extra_files(self, fake_repo: Path) -> None: + """``extra_files`` seeds additional paths (used for the merged recipe YAML).""" + dest = stage_workdir( + fake_repo, + include_paths=["nemo_rl"], + extra_files={"nrl_k8s_run.yaml": "policy:\n x: 1\n"}, + ) + try: + out = dest / "nrl_k8s_run.yaml" + assert out.is_file() + assert "policy:" in out.read_text() + finally: + shutil.rmtree(dest, ignore_errors=True) + + def test_single_file_include_copies_file(self, fake_repo: Path) -> None: + """An ``include_paths`` entry pointing at a file copies just that file.""" + dest = stage_workdir(fake_repo, include_paths=["tests/check_metrics.py"]) + try: + assert (dest / "tests" / "check_metrics.py").is_file() + finally: + shutil.rmtree(dest, ignore_errors=True) From 97183084f87368506c24c9cca7cdb70f10335991 Mon Sep 17 00:00:00 2001 From: Hemil Desai Date: Tue, 21 Apr 2026 10:47:04 -0700 Subject: [PATCH 26/84] feat(nrl-k8s): batch mode with kubectl exec + env macro + cluster dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a production-grade launch path, a dedicated dashboard command, and an onboarding guide. All opt-in — existing interactive dev flows keep working unchanged. `--mode {interactive, batch}` on `launch` / `run`: interactive (default): port-forward + working_dir upload + tail logs. batch: kubectl exec + code from image + nohup/disown + no wait. Each axis is independently overridable via `--submitter`, `--code-source`, `--code-path`, `--run-id`, `--wait/--no-wait`. New submitters package (`src/nrl_k8s/submitters/`): * JobSubmitter Protocol with submit/follow/status/stop. * PortForwardSubmitter (Ray Job SDK) — inlines env vars as shell `export`s instead of `runtime_env.env_vars` to sidestep Ray's "Failed to merge the Job's runtime env" error when ray.init captures os.environ. * ExecSubmitter — `kubectl cp` launcher script, `nohup bash ... & disown` via Popen, bounded-timeout pidfile poll (~17 s wall time vs 14 min previously for detach-clean return on flaky EKS). tail -F, kill -0 / exitcode sentinel, SIGTERM/SIGKILL stop. * SubmissionHandle cached under ~/.cache/nrl-k8s/runs/.json so job logs / job stop dispatch on transport without re-probing. Schema additions (back-compat — every default preserved): * SubmitSpec.submitter: portForward | exec * SubmitSpec.execTmpDir: /tmp * LaunchSpec.codeSource: upload | image | lustre * LaunchSpec.codePath + validator requiring it for image / lustre * LaunchSpec.runMode: interactive | batch * orchestrate.submit_training always injects NRL_K8S_RUN_ID into the entrypoint env so recipes can reference it for wandb name etc. `nrl-k8s cluster dashboard ` command: Port-forwards svc/-head-svc:8265 to localhost:8265 and opens a browser. `--fix` (default) runs a one-time reinstall of ray[default] with `--link-mode=copy` on the head pod when dashboard static assets are symlinks into the uv cache (aiohttp's follow_symlinks=False else 404s every JS/CSS file → blank page). The permanent fix is in the image build (`ENV UV_LINK_MODE=copy` before the ray install). Docs + examples: * docs/onboarding.md — step-by-step for standing up nrl-k8s on a fresh cluster (RBAC SA, pull secrets, wandb secret, node pool mapping, first run). * examples/qwen3_4b_if_gym_disagg.prod.infra.yaml — production variant of the canonical gym-disagg example: submitter=exec, codeSource=image, codePath=/opt/nemo-rl. * README: "Modes: interactive vs batch" section with the full batch walkthrough, env-var conventions, and blank-dashboard remediation. Testing: * 137 unit tests pass (was 134). New coverage: submitter protocol, exec command shape + env-escape + pidfile plumbing, port-forward back-compat, mode resolution, cluster dashboard invocation. Verified end-to-end against the live raycluster-single-qwen3-4b: * exec-mode run → SUCCEEDED, wandb: nemorl-single-k8s/runs/f9oisnfd * portForward+image run → SUCCEEDED, wandb: runs/wgm3txe7 Both submit wall-time 9–17 s, laptop detachable as soon as the driver is running. Signed-off-by: Hemil Desai Signed-off-by: Terry Kong --- tools/nrl_k8s/README.md | 168 +++++++ tools/nrl_k8s/docs/onboarding.md | 308 ++++++++++++ .../qwen3_4b_if_gym_disagg.prod.infra.yaml | 238 +++++++++ tools/nrl_k8s/src/nrl_k8s/cli.py | 455 ++++++++++++++++-- .../nrl_k8s/defaults/defaults.example.yaml | 12 + tools/nrl_k8s/src/nrl_k8s/k8s.py | 26 + tools/nrl_k8s/src/nrl_k8s/orchestrate.py | 126 ++++- tools/nrl_k8s/src/nrl_k8s/schema.py | 91 +++- .../src/nrl_k8s/submitters/__init__.py | 154 ++++++ tools/nrl_k8s/src/nrl_k8s/submitters/exec_.py | 374 ++++++++++++++ .../src/nrl_k8s/submitters/portforward.py | 154 ++++++ tools/nrl_k8s/tests/unit/test_cli.py | 182 +++++++ tools/nrl_k8s/tests/unit/test_schema.py | 42 ++ tools/nrl_k8s/tests/unit/test_submitters.py | 420 ++++++++++++++++ 14 files changed, 2690 insertions(+), 60 deletions(-) create mode 100644 tools/nrl_k8s/docs/onboarding.md create mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml create mode 100644 tools/nrl_k8s/src/nrl_k8s/submitters/__init__.py create mode 100644 tools/nrl_k8s/src/nrl_k8s/submitters/exec_.py create mode 100644 tools/nrl_k8s/src/nrl_k8s/submitters/portforward.py create mode 100644 tools/nrl_k8s/tests/unit/test_submitters.py diff --git a/tools/nrl_k8s/README.md b/tools/nrl_k8s/README.md index 5e81c4c53d6..d334ab3ea77 100644 --- a/tools/nrl_k8s/README.md +++ b/tools/nrl_k8s/README.md @@ -337,6 +337,174 @@ before a re-run (though `launch --replace` does this automatically). Not yet implemented — stubs print `not yet implemented (phase: ...)` and exit `2`. +## Modes: interactive vs batch + +`launch` and `run` both take `--mode {interactive, batch}`. The flag is a +macro — it flips a coherent set of defaults that a researcher would +otherwise pick individually. + +| dimension | `--mode interactive` (default) | `--mode batch` | +|---|---|---| +| Submitter transport | port-forward + Ray Job SDK | `kubectl exec` + `nohup` on head pod | +| Code source | `upload` (zipped working_dir via Ray SDK) | `image` (baked at `launch.codePath`) | +| Foreground wait | yes — tails logs, exits on terminal state | no — returns as soon as nohup fires | +| Laptop on critical path? | yes, for run lifetime | no | +| Typical use | dev iteration | long production runs | + +Each piece is independently overridable: `--submitter portForward`, +`--code-source {upload, image, lustre}`, `--code-path /opt/nemo-rl`, +`--run-id `, `--wait` / `--no-wait`. + +You can also set the mode in the infra YAML so researchers don't have +to remember the flag every run: + +```yaml +# recipe.infra.yaml +launch: + runMode: batch # flips defaults; --mode on CLI still wins + codeSource: image + codePath: /opt/nemo-rl +submit: + submitter: exec +``` + +### Batch submission walkthrough + +The canonical example is `qwen3_4b_if_gym_disagg.yaml` paired with its +production infra variant. Same recipe as the dev example above, but +submission goes through `kubectl exec` and code comes from +`/opt/nemo-rl` inside the container instead of a laptop upload. + +```bash +RECIPE=tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml +INFRA=tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml + +# Admin, once: bring up the two RayClusters. +nrl-k8s cluster up "$RECIPE" --infra "$INFRA" --role gym +nrl-k8s cluster up "$RECIPE" --infra "$INFRA" --role training + +# Researcher, per run — returns in seconds, laptop can close. +RUN_ID=qwen3-4b-gym-disagg-$(date +%Y%m%d-%H%M%S) +nrl-k8s launch "$RECIPE" --infra "$INFRA" --run-id "$RUN_ID" +# run id: qwen3-4b-gym-disagg-20260421-103012 +# kind: exec +# cluster: raycluster-gym-disagg-qwen3-4b (ns=nemo-rl-testing) +# pod: raycluster-gym-disagg-qwen3-4b-head-xyz42 +# tmp: /tmp/nrl-qwen3-4b-gym-disagg-20260421-103012 +# follow: nrl-k8s job logs $RUN_ID --role training -f + +# Observe from any laptop, any time — the handle is cached under +# ~/.cache/nrl-k8s/runs/.json. +nrl-k8s job logs "$RUN_ID" "$RECIPE" --infra "$INFRA" --role training -f +nrl-k8s job stop "$RUN_ID" "$RECIPE" --infra "$INFRA" --role training +``` + +The prod infra declares `submit.submitter: exec` + `launch.runMode: +batch` + `launch.codeSource: image`, so `--mode batch` is implicit. +The dev infra (`qwen3_4b_if_gym_disagg.infra.yaml`) keeps the +port-forward + upload path, letting `launch` / `run` default to +foreground log tailing for dev iteration. + +The exec submitter writes a launcher script onto the head pod, runs it +under `nohup` + `disown`, captures the PID and (on exit) the exitcode. +`job logs` streams the driver's stdout via `kubectl exec tail -F`; +`job stop` sends SIGTERM via `kubectl exec kill` (SIGKILL with +`--force`). + +Because the driver calls `ray.init(address="auto")` and registers with +the cluster, the run also shows up under `ray job list` on the head +dashboard and is reachable with `ray job logs` — a bonus if you prefer +the Ray CLI for log tailing. + +### Env vars and `$NRL_K8S_RUN_ID` + +Both submitters inject the resolved run id as `$NRL_K8S_RUN_ID` +(alongside anything in `infra.launch.env`) by prepending shell +`export` lines to the entrypoint. Recipes can reference it: + +```yaml +launch: + entrypoint: | + set -eu + cd /opt/nemo-rl + python -u examples/nemo_gym/run_grpo_nemo_gym.py \ + --config examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml \ + logger.wandb.name=my-run-$NRL_K8S_RUN_ID +``` + +Inlining envs as `export` (rather than `runtime_env.env_vars`) avoids +Ray's "Failed to merge the Job's runtime env" error, which triggers +when the head pod's captured `os.environ` conflicts with a submission's +declared env. The shell path is portable; the Ray path isn't. + +### Viewing the Ray dashboard + +``` +nrl-k8s cluster dashboard [-n ] +``` + +`` is what `nrl-k8s cluster list` / `kubectl get +rayclusters` shows. Namespace defaults to the current kube context. +Port-forwards `svc/-head-svc:8265` to `localhost:8265` +and opens the URL in the default browser. Ctrl+C kills the forward. + +**Blank dashboard?** The Ray dashboard serves its frontend from +`ray/dashboard/client/build/static/`. If those JS/CSS files are +symlinks into the uv cache (`/root/.cache/uv/archive-v0/...`), +aiohttp's `follow_symlinks=False` default 404s every request — the +browser renders an empty page. + +The command's default `--fix` detects this and reinstalls +`ray[default]==` in copy mode on the head pod +(`uv pip install --reinstall --link-mode=copy`). Idempotent: skipped +when the assets are already real files; ~30s when it runs. Pass +`--no-fix` on images that were already built correctly. + +**The permanent fix is in the image build.** Set +`UV_LINK_MODE=copy` *before* the `uv pip install` that brings in +`ray[default]`: + +```Dockerfile +ENV UV_LINK_MODE=copy +RUN uv pip install "ray[default]==2.54.0" ... +``` + +Or scope it to just the ray install: + +```Dockerfile +RUN uv pip install --link-mode=copy "ray[default]==2.54.0" +``` + +Setting `UV_LINK_MODE=copy` only at pod runtime has no effect on an +already-populated venv — existing symlinks stay symlinks until the +package is reinstalled (which is exactly what `--fix` does on demand). + +### Entrypoint shell + +Ray's Job submission API runs the entrypoint through `/bin/dash` by +default, which does not support `set -o pipefail` or bash arrays. If +your entrypoint needs bash-only syntax, either: + +- use `set -eu` instead of `set -euo pipefail`, or +- wrap the body: `exec bash -c ''`. + +The exec submitter runs the launcher under `bash` directly, so +bash-specific features work there regardless. + +### Code sources + +`launch.codeSource` picks what's on disk inside the pod at run time: + +| value | behaviour | when to use | +|---|---|---| +| `upload` (default) | Stage + upload `working_dir` via Ray Job SDK (100 MiB cap) | dev iteration with `--mode interactive` | +| `image` | Code baked into the container at `launch.codePath` (default `/opt/nemo-rl`) | production from a frozen image tag | +| `lustre` | Code pre-staged on a Lustre mount at `launch.codePath` | production with per-run snapshots without rebuilding | + +For `image` and `lustre`, the entrypoint is responsible for `cd`ing +into `codePath` and `source`ing any env (`3rdparty/vllm/nemo-rl.env`, +etc.) — the CLI does not inject `cd` for you. + ## `--replace` semantics Both `nrl-k8s launch` and `nrl-k8s run` accept `--replace`. It performs diff --git a/tools/nrl_k8s/docs/onboarding.md b/tools/nrl_k8s/docs/onboarding.md new file mode 100644 index 00000000000..5a8bb5d30ba --- /dev/null +++ b/tools/nrl_k8s/docs/onboarding.md @@ -0,0 +1,308 @@ +# Onboarding a new Kubernetes cluster + +Checklist for going from `kubectl config use-context ` to +`nrl-k8s launch` landing training. Walk through in order; each step +ends with a verify-it-worked probe. + +--- + +## 1. Baseline cluster capabilities + +Required — refuses to install onto clusters that lack these. + +```bash +# KubeRay operator +kubectl get crd rayclusters.ray.io -o jsonpath='{.metadata.name}' && echo " ✓" +kubectl -n kuberay-system get deploy kuberay-operator \ + -o jsonpath='{.status.conditions[?(@.type=="Available")].status}' +# → True + +# GPU device plugin (at least one node reports nvidia.com/gpu in allocatable) +kubectl get nodes \ + -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.allocatable.nvidia\.com/gpu}{"\n"}{end}' +# → one or more rows with non-empty GPU counts +``` + +If KubeRay isn't present, install it. The operator helm chart we use +is tracked in this repo under `infra/helm/`; on a fresh cluster: + +```bash +helm repo add kuberay https://ray-project.github.io/kuberay-helm/ +helm upgrade --install kuberay-operator kuberay/kuberay-operator \ + --namespace kuberay-system --create-namespace \ + --version 1.5.1 +``` + +If your GPU nodes don't yet advertise `nvidia.com/gpu`, install the +NVIDIA device plugin (`kubectl apply -f +https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.15.0/nvidia-device-plugin.yml`) +and re-run the `kubectl get nodes` check. + +--- + +## 2. Create the working namespace + +```bash +NS=nemo-rl-testing # pick whatever fits your org +kubectl create namespace $NS +kubectl config set-context --current --namespace=$NS +``` + +`nrl-k8s` reads the active namespace from your kube context by +default, so setting it on the context saves you from `-n $NS` on +every call. + +--- + +## 3. RBAC — the endpoint-registry ServiceAccount + +Disaggregated runs rendezvous on a ConfigMap called +`nemo-rl-endpoints-`, written and read by the training + +generation + gym pods via the Kubernetes Python client. Each pod +needs `get / list / watch / create / update / patch` on ConfigMaps +in its namespace, and (if the peer-watcher sidecar is enabled) +`get / delete` on `rayclusters.ray.io` too. + +Apply the bundled RBAC manifest: + +```bash +kubectl apply -n $NS -f - <<'EOF' +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nemo-rl-endpoint-registry +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: nemo-rl-endpoint-registry +rules: + - apiGroups: [""] + resources: [configmaps] + verbs: [get, list, watch, create, update, patch, delete] + - apiGroups: [ray.io] + resources: [rayclusters] + verbs: [get, list, watch, delete] +EOF +kubectl create rolebinding -n $NS nemo-rl-endpoint-registry \ + --role=nemo-rl-endpoint-registry \ + --serviceaccount=$NS:nemo-rl-endpoint-registry +``` + +Verify: + +```bash +kubectl auth can-i create configmaps --as=system:serviceaccount:$NS:nemo-rl-endpoint-registry +kubectl auth can-i delete rayclusters.ray.io --as=system:serviceaccount:$NS:nemo-rl-endpoint-registry +# → yes, yes +``` + +--- + +## 4. Image pull secrets (private registries) + +If your image lives in a private registry (e.g. `nvcr.io/nvidian/...`), +create a Docker-config secret: + +```bash +kubectl create secret docker-registry ngc-registry-secret \ + --docker-server=nvcr.io \ + --docker-username='$oauthtoken' \ + --docker-password="$NGC_API_KEY" \ + -n $NS +``` + +The recipe's `infra.imagePullSecrets` field references these by name: + +```yaml +imagePullSecrets: [ngc-registry-secret] +``` + +--- + +## 5. W&B secret (optional) + +If your recipe logs to Weights & Biases, pods need `WANDB_API_KEY`. +We consume it via a `secretKeyRef` rather than embedding the key in +YAML — so rotate/replace the secret without touching any recipe: + +```bash +kubectl create secret generic wandb-api-key \ + --from-literal=WANDB_API_KEY="$WANDB_API_KEY" \ + -n $NS +``` + +The existing example infra files reference `secretKeyRef: {name: +wandb-api-key, key: WANDB_API_KEY}` on the training head container. + +--- + +## 6. Node pool — labels, taints, GPU resources + +RayCluster specs pin workers via `nodeSelector` + `tolerations`. The +bundled examples use two keys that are NVIDIA-internal (`gpu-wrangler.nvidia.com/lease` +and `platform.nvidia.com/gpu`); you'll almost certainly want to swap +those for whatever your GPU node pool advertises. + +```bash +# Inspect an existing GPU node: +kubectl get node -o jsonpath='{.metadata.labels}' | python3 -m json.tool +kubectl get node -o jsonpath='{.spec.taints}' | python3 -m json.tool +``` + +Then update `clusters.training.spec.*.template.spec.nodeSelector` +and `.tolerations` in your recipe's `.infra.yaml` to match. The +examples use YAML anchors (`&shared_node_selector`) so the selector +lives in one place per file. + +The GPU worker's resource request must include: + +```yaml +resources: + limits: + cpu: "176" # leave headroom for daemonsets + memory: "1800Gi" # ditto + nvidia.com/gpu: "8" # must equal rayStartParams.num-gpus + vpc.amazonaws.com/efa: "32" # AWS p5 only; drop on other clouds +``` + +On AWS EKS with p5 instances you also need the EFA device plugin +installed in the cluster (`https://github.com/aws/eks-charts/tree/main/stable/aws-efa-k8s-device-plugin`). +On non-AWS clouds, remove the EFA line entirely. + +--- + +## 7. Sanity-check with `nrl-k8s` + +Install the CLI if you haven't already: + +```bash +uv tool install ./tools/nrl_k8s +# or for editable dev: cd tools/nrl_k8s && uv pip install -e ".[test]" +nrl-k8s --version +``` + +Load-test a recipe without hitting the API server: + +```bash +nrl-k8s check \ + tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml +``` + +This validates schema, resolves the full infra, and prints a +one-page summary. No cluster calls. + +--- + +## 8. Bring up a RayCluster + +```bash +nrl-k8s cluster up \ + tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ + --role training +``` + +The command applies the RayCluster CR and waits for +`.status.state == ready`. Typical times: 1–2 min image pull + +scheduling, then steady-state. + +Check the head pod came up: + +```bash +nrl-k8s cluster list +# → raycluster-single-qwen3-4b ready +kubectl get pods -n $NS \ + -l ray.io/cluster=raycluster-single-qwen3-4b +# → both head + worker showing Running +``` + +--- + +## 9. Browse the Ray dashboard + +```bash +nrl-k8s cluster dashboard raycluster-single-qwen3-4b +``` + +Port-forwards `localhost:8265` and opens your browser. First run +auto-fixes a known uv-symlink issue on the head pod (~30 s one-time +reinstall of `ray[default]` in copy mode). Pass `--no-fix` if your +image was built with `ENV UV_LINK_MODE=copy` already. + +If the dashboard still renders blank, see the "Blank dashboard" +section in `README.md` — the permanent fix is in the image build. + +--- + +## 10. Submit a dry run + +Pick a mode up front: + +- **interactive** (default): laptop stays attached, tails logs, exits + on terminal state. Good for "does my recipe even start." Uploads a + `working_dir` via Ray's SDK (100 MiB cap). +- **batch**: `kubectl exec` + `nohup` on the training head pod. Code + must already be on disk in the pod (`launch.codeSource=image` or + `lustre`). Returns in <30 s and the laptop can disconnect. Use this + for real production runs. + +Dev iteration: + +```bash +nrl-k8s run \ + tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ + --mode interactive grpo.max_num_steps=2 +``` + +Production (using the prod-mode variant of the gym-disagg example): + +```bash +nrl-k8s launch \ + tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ + --run-id smoke-$(date +%s) +``` + +Observability is transport-aware: + +```bash +nrl-k8s job logs --infra --role training -f +nrl-k8s job stop --infra --role training +``` + +--- + +## 11. Clean up + +```bash +# Stop a specific run: +nrl-k8s job stop --infra --role training + +# Tear down the RayCluster when you're done: +nrl-k8s cluster down --infra --role training +# or by name: +nrl-k8s cluster down --name raycluster-single-qwen3-4b +``` + +--- + +## Per-cluster fixture summary + +| Thing | Where it lives | Source of truth | +|---|---|---| +| KubeRay operator | `kuberay-system` namespace | `infra/helm/helmfile.yaml` | +| EFA / GPU device plugins | `kube-system` namespace | `infra/helm/` | +| Your working namespace | cluster root | `kubectl create namespace` | +| `nemo-rl-endpoint-registry` SA + Role | per-namespace | RBAC snippet in §3 | +| `ngc-registry-secret` pull secret | per-namespace | `kubectl create secret docker-registry` | +| `wandb-api-key` secret | per-namespace | `kubectl create secret generic` | +| Node pool selectors / tolerations | per-cluster node labels | update `.infra.yaml` `_shared` anchors | + +The `.infra.yaml` file is the single place per recipe where +cluster-specific values live. Once you've adapted the examples to +your cluster, commit them somewhere your team can share (a team +`infra/` repo, or the `nrl_k8s/examples/` dir if you contribute +back). diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml new file mode 100644 index 00000000000..22e046ae4ef --- /dev/null +++ b/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml @@ -0,0 +1,238 @@ +# Production-batch variant of qwen3_4b_if_gym_disagg.infra.yaml. +# +# Same recipe, same two RayClusters (training + gym), same runtime +# behaviour — but submission goes through `kubectl exec` into the +# training head pod and the code lives at /opt/nemo-rl inside the +# image. No working_dir upload; the submitting laptop can disconnect +# as soon as nohup fires. +# +# Usage: +# # Admins, once: bring up the two RayClusters. +# nrl-k8s cluster up tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ +# --role gym +# nrl-k8s cluster up tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ +# --role training +# +# # Researcher, per run — returns in seconds, laptop disconnectable. +# nrl-k8s launch tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ +# --run-id qwen3-4b-gym-disagg-$(date +%Y%m%d-%H%M%S) +# +# # Observe from anywhere; handle is cached on disk. +# nrl-k8s job logs \ +# tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ +# --role training -f + +_shared: + nodeSelector: &shared_node_selector + gpu-wrangler.nvidia.com/lease: nemo-rl-testing + tolerations: &shared_tolerations + - {key: gpu-wrangler.nvidia.com/lease, operator: Equal, value: nemo-rl-testing, effect: NoSchedule} + - {key: platform.nvidia.com/gpu, operator: Equal, value: "true", effect: NoSchedule} + headEnv: &shared_head_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + workerEnv: &shared_worker_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + headResources: &shared_head_resources + limits: {cpu: "8", memory: "32Gi"} + requests: {cpu: "2", memory: "8Gi"} + gpuWorkerResources: &gpu_worker_resources + limits: + cpu: "176" + memory: "1800Gi" + nvidia.com/gpu: "8" + vpc.amazonaws.com/efa: "32" + requests: + cpu: "176" + memory: "1800Gi" + nvidia.com/gpu: "8" + vpc.amazonaws.com/efa: "32" + gpuHeadPorts: &gpu_head_ports + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + dshmMounts: &dshm_mounts + - {mountPath: /dev/shm, name: dshm} + dshm16: &dshm_16 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 16Gi} + dshm64: &dshm_64 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 64Gi} + dshm4: &dshm_4 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 4Gi} + +namespace: nemo-rl-testing +image: nvcr.io/nvidian/nemo-rl:nightly +imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] +serviceAccount: nemo-rl-endpoint-registry + +submit: + # Shell into the training head pod instead of port-forwarding the + # dashboard to the laptop. Once `nohup` + `disown` fire the laptop is + # off the critical path. + submitter: exec + execTmpDir: /tmp + +launch: + mode: attach + # runMode: batch flips defaults without --mode on the CLI. + runMode: batch + # Code is baked into the container at /opt/nemo-rl (the + # nvcr.io/nvidian/nemo-rl:nightly build ships it). No working_dir + # upload, no .gitignore strip, no 100 MiB cap. + codeSource: image + codePath: /opt/nemo-rl + attach: + gym: raycluster-gym-disagg-gym-qwen3-4b + training: raycluster-gym-disagg-qwen3-4b + peerWatcher: false + env: {} + # Entrypoint runs inside the training head pod under `nohup bash`. + # Env vars here propagate to Ray's worker actors via the standard + # os.environ capture — no runtime_env.env_vars gymnastics. + entrypoint: | + # Keep POSIX-compatible — Ray's port-forward path submits entrypoints + # through /bin/dash, which doesn't support `set -o pipefail`. + set -eu + cd /opt/nemo-rl + export GLOO_SOCKET_IFNAME=enp71s0 + export NCCL_SOCKET_IFNAME=enp71s0 + export FI_PROVIDER=efa + export DISAGG_TRAIN_PATH=/tmp/train.jsonl + export DISAGG_VALID_PATH=/tmp/validation.jsonl + export RAY_enable_infeasible_task_early_exit=true + export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM + + python -u examples/nemo_gym/run_grpo_nemo_gym.py \ + --config examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml \ + +env.disagg_job_id=qwen3-4b-gym-disagg \ + ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ + ~policy.optimizer.kwargs.foreach \ + ~policy.optimizer.kwargs.fused + +clusters: + # ---------------- Gym (CPU, head-only) ---------------- + # Gym daemon still submits via port-forward + working_dir upload — + # it's a long-lived server admins bring up once, so the upload cost + # is paid once per `cluster up` and the researcher's `launch` never + # touches it. + gym: + name: raycluster-gym-disagg-gym-qwen3-4b + daemon: + submissionId: qwen3-4b-gym-disagg-server-v5 + entrypoint: | + set -eu + ROOT_DIR=$(pwd) + GYM_DIR=\${ROOT_DIR}/3rdparty/Gym-workspace/Gym + export PYTHONPATH=\${ROOT_DIR}:\${GYM_DIR}:\${PYTHONPATH-} + cd \${GYM_DIR} + mkdir -p cache + RAY_VERSION=$(python -c "import ray; print(ray.__version__)") + echo "ray==\${RAY_VERSION}" > /tmp/ray-pin.txt + pip install -e "." --constraint /tmp/ray-pin.txt -q + pip install kubernetes -q + exec python -m nemo_gym.standalone_server \ + --job-id qwen3-4b-gym-disagg \ + --port 9090 \ + --model-name Qwen/Qwen3-4B-Instruct-2507 \ + --config-yaml \${ROOT_DIR}/infra/examples/gym_standalone_config_instruction_following.yaml + rayUploadPaths: + - nemo_rl + - infra/examples + - 3rdparty/Gym-workspace/Gym/nemo_gym + - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model + - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent + - 3rdparty/Gym-workspace/Gym/pyproject.toml + - 3rdparty/Gym-workspace/Gym/uv.lock + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/app.py + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/requirements.txt + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/tests + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/validation.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example_rollouts.jsonl + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {num-gpus: "0", dashboard-host: "0.0.0.0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-head + env: + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + resources: *shared_head_resources + ports: + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + - {containerPort: 9090, name: gym-server} + volumeMounts: *dshm_mounts + volumes: *dshm_4 + + # ---------------- Training (GPU head + worker, generation colocated) ---------------- + training: + name: raycluster-gym-disagg-qwen3-4b + labels: + disagg.nemo-rl/cluster: gym-disagg-qwen3-4b + disagg.nemo-rl/run: qwen3-4b-gym-disagg + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-head + env: + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: + name: wandb-api-key + key: WANDB_API_KEY + resources: *shared_head_resources + ports: *gpu_head_ports + volumeMounts: *dshm_mounts + volumes: *dshm_16 + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: {num-gpus: "8", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-worker + env: *shared_worker_env + resources: *gpu_worker_resources + volumeMounts: *dshm_mounts + volumes: *dshm_64 diff --git a/tools/nrl_k8s/src/nrl_k8s/cli.py b/tools/nrl_k8s/src/nrl_k8s/cli.py index 0b7664403b8..15307ffb763 100644 --- a/tools/nrl_k8s/src/nrl_k8s/cli.py +++ b/tools/nrl_k8s/src/nrl_k8s/cli.py @@ -20,7 +20,7 @@ from . import __version__ from .config import LoadedConfig, load_recipe_with_infra from .orchestrate import ALL_ROLES -from .schema import ClusterSpec +from .schema import ClusterSpec, CodeSource, RunMode, SubmitterMode _INFRA_OPTION = click.option( "--infra", @@ -31,6 +31,124 @@ "contain an `infra:` key.", ) _ROLE_CHOICE = click.Choice(list(ALL_ROLES)) +_MODE_CHOICE = click.Choice([m.value for m in RunMode]) +_SUBMITTER_CHOICE = click.Choice([m.value for m in SubmitterMode]) +_CODE_SOURCE_CHOICE = click.Choice([m.value for m in CodeSource]) + + +# Macro -> (submitter, codeSource, no_wait). CLI --mode wins over +# infra.launch.runMode; explicit --submitter / --code-source / +# --wait/--no-wait flags win over both. +_MODE_DEFAULTS: dict[RunMode, tuple[SubmitterMode, CodeSource, bool]] = { + RunMode.INTERACTIVE: (SubmitterMode.PORT_FORWARD, CodeSource.UPLOAD, False), + RunMode.BATCH: (SubmitterMode.EXEC, CodeSource.IMAGE, True), +} + + +def _resolve_mode_defaults( + *, + cli_mode: str | None, + infra_mode: RunMode, + cli_submitter: str | None, + cli_code_source: str | None, + cli_wait: bool | None, +) -> tuple[RunMode, SubmitterMode, CodeSource, bool]: + """Return (resolved_mode, submitter, code_source, no_wait). + + ``cli_wait`` is the tri-state carried by click's ``--wait/--no-wait`` + flag pair (True / False / None=unset). + """ + mode = RunMode(cli_mode) if cli_mode else infra_mode + default_submitter, default_code_src, default_no_wait = _MODE_DEFAULTS[mode] + submitter = SubmitterMode(cli_submitter) if cli_submitter else default_submitter + code_src = CodeSource(cli_code_source) if cli_code_source else default_code_src + if cli_wait is None: + no_wait = default_no_wait + else: + no_wait = not cli_wait + return mode, submitter, code_src, no_wait + + +def _apply_mode_overrides( + loaded: LoadedConfig, + *, + submitter: SubmitterMode, + code_source: CodeSource, + code_path: str | None, +) -> None: + """Mutate the loaded InfraConfig so downstream sees the resolved values. + + `_resolve_mode_defaults` produces the final submitter/codeSource; we + push those into the pydantic model so `orchestrate.submit_training` + and `build_submitter` read one source of truth. `code_path` overrides + `launch.codePath` when set; else the infra YAML keeps its value. + """ + # Pydantic models are immutable by default; use model_copy via deep set. + infra = loaded.infra + infra.submit.submitter = submitter + infra.launch.codeSource = code_source + if code_path is not None: + infra.launch.codePath = code_path + # Re-run the validator manually so codePath-required rule fires with + # the effective values. + if code_source in (CodeSource.IMAGE, CodeSource.LUSTRE) and not infra.launch.codePath: + _cli_error( + f"--code-source {code_source.value} requires --code-path (or infra.launch.codePath)", + hint="pass --code-path /opt/nemo-rl (image default) or a Lustre mount path", + ) + + +# Shared decorator factory for the flag block added to both launch and run. +def _mode_options(fn): + fn = click.option( + "--mode", + "cli_mode", + type=_MODE_CHOICE, + default=None, + help="Macro: interactive = port-forward + working_dir upload + tail " + "(dev default). batch = kubectl exec + code from image + no wait " + "(production). Overrides infra.launch.runMode.", + )(fn) + fn = click.option( + "--submitter", + "cli_submitter", + type=_SUBMITTER_CHOICE, + default=None, + help="Transport for the training entrypoint. Overrides --mode's default.", + )(fn) + fn = click.option( + "--code-source", + "cli_code_source", + type=_CODE_SOURCE_CHOICE, + default=None, + help="Where the code lives. `upload` stages a working_dir from the " + "laptop; `image` / `lustre` expect code on disk inside the pod.", + )(fn) + fn = click.option( + "--code-path", + "cli_code_path", + type=str, + default=None, + help="Absolute container path for code when --code-source is " + "image or lustre. Overrides infra.launch.codePath.", + )(fn) + fn = click.option( + "--run-id", + "cli_run_id", + type=str, + default=None, + help="Human-readable tag for this run. Used as the Ray submission " + "id (port-forward) or pidfile directory name (exec). Defaults to " + "`training-`.", + )(fn) + fn = click.option( + "--wait/--no-wait", + "cli_wait", + default=None, + help="Override mode's wait default: --wait tails logs and exits " + "on terminal state; --no-wait returns immediately after submit.", + )(fn) + return fn # ============================================================================= @@ -237,28 +355,34 @@ def validate(ctx, recipe, overrides, infra_path) -> None: show_default="cwd", help="NeMo-RL repo root used to source files for the working_dir upload.", ) -@click.option( - "--follow", - is_flag=True, - help="Stream training-job logs after submit.", -) @click.option( "--replace", is_flag=True, help="Stop any running training job on the cluster before submitting.", ) +@_mode_options def launch( recipe: Path, overrides: tuple[str, ...], infra_path: Path | None, repo_root: Path, - follow: bool, replace: bool, + cli_mode: str | None, + cli_submitter: str | None, + cli_code_source: str | None, + cli_code_path: str | None, + cli_run_id: str | None, + cli_wait: bool | None, ) -> None: """Submit a training job against an already-up training cluster. - ``--replace`` stops any RUNNING Ray Job on the training cluster first so - the new submission doesn't queue behind GPU-holding stragglers. + ``--mode interactive`` (default) uses port-forward + working_dir + upload and tails logs. ``--mode batch`` uses kubectl exec + in-image + code, returns as soon as the driver is running via nohup, and the + laptop can disconnect. + + ``--replace`` stops any RUNNING Ray Job on the training cluster first + so the new submission doesn't queue behind GPU-holding stragglers. """ from . import orchestrate from . import submit as submit_mod @@ -271,17 +395,35 @@ def launch( "infra.launch.entrypoint is empty", hint="launch command requires infra.launch.entrypoint; see docs/recipes.md", ) + + mode, submitter, code_src, no_wait = _resolve_mode_defaults( + cli_mode=cli_mode, + infra_mode=loaded.infra.launch.runMode, + cli_submitter=cli_submitter, + cli_code_source=cli_code_source, + cli_wait=cli_wait, + ) + _apply_mode_overrides(loaded, submitter=submitter, code_source=code_src, code_path=cli_code_path) + click.echo( + f"[launch] mode={mode.value} submitter={submitter.value} " + f"code_source={code_src.value} no_wait={no_wait}", + err=True, + ) + try: result = orchestrate.submit_training( - loaded, log=click.echo, repo_root=repo_root.resolve(), replace=replace + loaded, + log=click.echo, + repo_root=repo_root.resolve(), + replace=replace, + run_id=cli_run_id, ) except Exception as exc: # noqa: BLE001 _explain_and_exit(exc, context="launch failed") - click.echo(f"training job: {result.training_job_id}") - click.echo(f"dashboard: {result.training_dashboard}") - if follow: - _tail(result.training_dashboard, result.training_job_id) + _emit_handle(result.handle) + if not no_wait: + _follow_handle(result.handle) @main.command() @@ -295,23 +437,24 @@ def launch( show_default="cwd", help="NeMo-RL repo root used to source files for the working_dir upload.", ) -@click.option( - "--follow", - is_flag=True, - help="Stream training-job logs after submit.", -) @click.option( "--replace", is_flag=True, help="Stop any running daemon/training job before submitting new ones.", ) +@_mode_options def run( recipe: Path, overrides: tuple[str, ...], infra_path: Path | None, repo_root: Path, - follow: bool, replace: bool, + cli_mode: str | None, + cli_submitter: str | None, + cli_code_source: str | None, + cli_code_path: str | None, + cli_run_id: str | None, + cli_wait: bool | None, ) -> None: """Bring up every cluster + daemon declared in the recipe, then submit training. @@ -319,10 +462,14 @@ def run( RayCluster, submit its daemon (for generation/gym), then submit the training entrypoint against the training cluster. - ``--replace`` stops any previous RUNNING instance of a daemon or training - job before submitting (and suffixes daemon submissionIds with a - timestamp so Ray accepts the resubmit). Use it to re-run with code - changes without manually bumping IDs or calling ``job stop``. + ``--mode batch`` is the production shape: bring up the training + cluster, exec into its head, start the entrypoint under nohup, and + return. Pair with ``--code-source image`` or ``--code-source lustre`` + to skip the laptop-side working_dir upload entirely. + + ``--replace`` stops any previous RUNNING instance of a daemon or + training job before submitting (and suffixes daemon submissionIds + with a timestamp so Ray accepts the resubmit). """ from . import orchestrate from . import submit as submit_mod @@ -330,17 +477,35 @@ def run( loaded = _load_or_exit(recipe, overrides, infra_path) if not submit_mod.is_in_cluster(): _preflight_or_exit(loaded.infra.namespace) + + mode, submitter, code_src, no_wait = _resolve_mode_defaults( + cli_mode=cli_mode, + infra_mode=loaded.infra.launch.runMode, + cli_submitter=cli_submitter, + cli_code_source=cli_code_source, + cli_wait=cli_wait, + ) + _apply_mode_overrides(loaded, submitter=submitter, code_source=code_src, code_path=cli_code_path) + click.echo( + f"[run] mode={mode.value} submitter={submitter.value} " + f"code_source={code_src.value} no_wait={no_wait}", + err=True, + ) + try: result = orchestrate.run( - loaded, log=click.echo, repo_root=repo_root.resolve(), replace=replace + loaded, + log=click.echo, + repo_root=repo_root.resolve(), + replace=replace, + run_id=cli_run_id, ) except Exception as exc: # noqa: BLE001 _explain_and_exit(exc, context="run failed") - click.echo(f"training job: {result.training_job_id}") - click.echo(f"dashboard: {result.training_dashboard}") - if follow: - _tail(result.training_dashboard, result.training_job_id) + _emit_handle(result.handle) + if not no_wait: + _follow_handle(result.handle) @main.command() @@ -604,6 +769,144 @@ def cluster_list(namespace: str | None) -> None: click.echo(f"{name}\t{state}") +@cluster.command("dashboard") +@click.argument("name") +@click.option( + "--namespace", + "-n", + default=None, + help="Kubernetes namespace. Defaults to the current kube context's namespace.", +) +@click.option( + "--port", + "local_port", + type=int, + default=8265, + show_default=True, + help="Local port to bind the forward to.", +) +@click.option( + "--open/--no-open", + "open_browser", + default=True, + show_default=True, + help="Open the dashboard URL in a browser once the forward is up.", +) +@click.option( + "--fix/--no-fix", + "auto_fix", + default=True, + show_default=True, + help="If Ray's dashboard static assets are symlinks on the head pod " + "(uv install default), reinstall ray[default] with --link-mode=copy " + "before forwarding. Pass --no-fix on images already built with " + "UV_LINK_MODE=copy.", +) +def cluster_dashboard( + name: str, + namespace: str | None, + local_port: int, + open_browser: bool, + auto_fix: bool, +) -> None: + """Port-forward a RayCluster's dashboard (and fix it if blank). + + ``NAME`` is the RayCluster name (as shown by ``nrl-k8s cluster list`` + or ``kubectl get rayclusters``). No recipe / infra YAML required. + + Does everything in one go: + + 1. Resolve the head pod for ``NAME``. + 2. If ``--fix`` (default): check for symlinked dashboard assets and, + if any are present, ``uv pip install --reinstall --link-mode=copy + ray[default]`` on the head pod so aiohttp can actually serve the + JS/CSS (the assets are otherwise 404 → blank page). + 3. ``kubectl port-forward svc/-head-svc :8265``. + 4. Open ``http://localhost:`` in the default browser. + 5. Ctrl+C kills the forward; the cluster keeps running. + + The permanent fix is in the image build — ``ENV UV_LINK_MODE=copy`` + before the first ``uv pip install`` step in your Dockerfile. The + auto-fix here is a convenience for images without that flag. + """ + import time + import webbrowser + from . import submit as submit_mod + from .config import _infer_kube_namespace + + ns = namespace or _infer_kube_namespace() + if not submit_mod.is_in_cluster(): + _preflight_or_exit(ns) + + if auto_fix: + _reinstall_ray_if_symlinked(name, ns) + + url = f"http://localhost:{local_port}" + pf = submit_mod._PortForward(name, ns, local_port) + click.echo(f"[dashboard] forwarding {name} head :8265 → {url}") + try: + pf.start() + except Exception as exc: # noqa: BLE001 + _explain_and_exit(exc, context="dashboard port-forward failed") + if open_browser: + webbrowser.open(url) + click.echo("[dashboard] Ctrl+C to stop.") + try: + while pf.alive(): + time.sleep(1) + except KeyboardInterrupt: + click.echo("\n[dashboard] stopping forward.") + finally: + pf.stop() + + +def _reinstall_ray_if_symlinked(cluster_name: str, namespace: str) -> None: + """Reinstall ray[default] in copy mode when its assets are symlinks. + + Checks for any symlink under Ray's dashboard build dir and, if one + exists, runs ``uv pip install --reinstall --link-mode=copy + ray[default]==`` to replace every symlink in the package + with a real file. Idempotent: no-op when the dir has no symlinks. + """ + import subprocess as _sp + + from . import k8s + + pod = k8s.get_head_pod(cluster_name, namespace) + script = ( + "set -eu\n" + "BUILD=$(python3 -c 'import ray, os; " + "print(os.path.join(os.path.dirname(ray.__file__)," + '"dashboard/client/build"))\' 2>/dev/null)\n' + 'if [ -z "$BUILD" ] || [ ! -d "$BUILD" ]; then\n' + ' echo "[dashboard] ray install not found on pod; skipping fix"\n' + " exit 0\n" + "fi\n" + 'if ! find "$BUILD" -type l -print -quit 2>/dev/null | grep -q .; then\n' + ' echo "[dashboard] assets already real files; no fix needed"\n' + " exit 0\n" + "fi\n" + "VER=$(python3 -c 'import ray; print(ray.__version__)')\n" + 'echo "[dashboard] reinstalling ray[default]==$VER with --link-mode=copy (~30s) ..."\n' + "UV=$(command -v uv || echo /opt/nemo_rl_venv/bin/uv)\n" + '"$UV" pip install --reinstall --link-mode=copy --quiet "ray[default]==$VER"\n' + 'echo "[dashboard] reinstall complete."\n' + ) + cmd = [ + "kubectl", "exec", "-n", namespace, pod.metadata.name, "--", + "bash", "-c", script, + ] + try: + res = _sp.run(cmd, check=False, capture_output=True, text=True, timeout=180) + except (_sp.TimeoutExpired, FileNotFoundError) as exc: + click.echo(f"[dashboard] fix skipped: {exc}", err=True) + return + for line in (res.stdout or "").splitlines(): + click.echo(line) + if res.returncode != 0 and (res.stderr or "").strip(): + click.echo(f"[dashboard] stderr: {res.stderr.strip()}", err=True) + + # ---- `job` group -------------------------------------------------------- @@ -664,16 +967,37 @@ def job_list( required=True, help="Which cluster hosts the job.", ) +@click.option( + "-f", "--follow", is_flag=True, help="Stream new output until Ctrl+C." +) def job_logs( submission_id: str, recipe: Path, overrides: tuple[str, ...], infra_path: Path | None, role: str, + follow: bool, ) -> None: - """Stream logs for a Ray Job by submission id on a given role's cluster.""" + """Stream logs for a submitted run by its id on a given role's cluster. + + Dispatches on the cached handle (``~/.cache/nrl-k8s/runs/.json``): + port-forward handles go through Ray's log tail API; exec handles go + through ``kubectl exec … tail -F`` on the head pod's stdout file. + + When no cached handle exists we fall back to the Ray dashboard — so + this command keeps working against jobs submitted by older CLI + versions or by ``ray job submit`` directly. + """ + del follow # always follows — flag kept for back-compat / readability + from .submitters import load_handle + loaded = _load_or_exit(recipe, overrides, infra_path) cluster = _pick_cluster_or_exit(loaded, role) + + handle = load_handle(submission_id) + if handle is not None and handle.kind == "exec": + _follow_handle(handle) + return _tail_daemon(cluster.name, loaded.infra.namespace, submission_id) @@ -688,20 +1012,44 @@ def job_logs( required=True, help="Which cluster hosts the job.", ) +@click.option( + "--force", is_flag=True, help="Exec mode only: send SIGKILL instead of SIGTERM." +) def job_stop( submission_id: str, recipe: Path, overrides: tuple[str, ...], infra_path: Path | None, role: str, + force: bool, ) -> None: - """Stop a Ray Job by submission id.""" - from ray.job_submission import JobSubmissionClient + """Stop a submitted run by id. - from . import submit + Transport-aware via the cached handle — Ray jobs go through + ``stop_job``; exec runs are killed with SIGTERM (or SIGKILL with + ``--force``). Falls back to Ray's API when no cached handle exists. + """ + from .submitters import load_handle loaded = _load_or_exit(recipe, overrides, infra_path) cluster = _pick_cluster_or_exit(loaded, role) + + handle = load_handle(submission_id) + if handle is not None and handle.kind == "exec": + from .submitters.exec_ import ExecSubmitter + + tmp_root = (handle.tmp_dir or "/tmp/nrl-x").rsplit("/", 1)[0] or "/tmp" + try: + ExecSubmitter(exec_tmp_dir=tmp_root).stop(handle, force=force) + except Exception as exc: # noqa: BLE001 + _explain_and_exit(exc, context=f"stop {submission_id} failed") + click.echo(f"stopped {submission_id} (exec)") + return + + from ray.job_submission import JobSubmissionClient + + from . import submit + try: with submit.dashboard_url(cluster.name, loaded.infra.namespace) as dash: clnt = JobSubmissionClient(dash) @@ -803,6 +1151,47 @@ def _tail_daemon(cluster_name: str, namespace: str, submission_id: str) -> None: _explain_and_exit(exc, context=f"tailing {submission_id} failed") +def _emit_handle(handle) -> None: # type: ignore[no-untyped-def] + """Print the resolved handle + next-step commands to stdout. + + Kept close to the submit call sites so the user sees a coherent + "here's what you submitted, here's how to follow it" block in both + interactive and batch flows. + """ + click.echo(f"run id: {handle.run_id}") + click.echo(f"kind: {handle.kind}") + click.echo(f"cluster: {handle.cluster_name} (ns={handle.namespace})") + if handle.kind == "exec": + click.echo(f"pod: {handle.pod}") + click.echo(f"tmp: {handle.tmp_dir}") + click.echo( + f"follow: nrl-k8s job logs {handle.run_id} " + f" --role training -f" + ) + click.echo( + f"stop: nrl-k8s job stop {handle.run_id} " + f" --role training" + ) + + +def _follow_handle(handle) -> None: # type: ignore[no-untyped-def] + """Stream logs for a handle using whichever transport submitted it.""" + from .submitters import build_submitter + from .schema import SubmitterMode + + class _Stub: # minimal infra shim so build_submitter picks the right transport + class submit: + submitter = SubmitterMode.EXEC if handle.kind == "exec" else SubmitterMode.PORT_FORWARD + execTmpDir = handle.tmp_dir.rsplit("/", 1)[0] if (handle.kind == "exec" and handle.tmp_dir) else "/tmp" + + submitter = build_submitter(_Stub) # type: ignore[arg-type] + try: + for line in submitter.follow(handle): + click.echo(line, nl=False) + except KeyboardInterrupt: + click.echo("\n(interrupted — run continues)", err=True) + + def _first_worker_pod_or_exit(cluster_name: str, namespace: str) -> str: from . import inspect as ins diff --git a/tools/nrl_k8s/src/nrl_k8s/defaults/defaults.example.yaml b/tools/nrl_k8s/src/nrl_k8s/defaults/defaults.example.yaml index 83c18d4cef7..258d5b920ac 100644 --- a/tools/nrl_k8s/src/nrl_k8s/defaults/defaults.example.yaml +++ b/tools/nrl_k8s/src/nrl_k8s/defaults/defaults.example.yaml @@ -46,8 +46,20 @@ infra: portForward: auto devPod: auto localDashboardPort: 18265 + # Transport: portForward = Ray Job SDK via kubectl port-forward; + # exec = kubectl exec + nohup python (for batch production). + submitter: portForward + # Pod path ExecSubmitter uses for launcher + stdout + pidfile. + execTmpDir: /tmp launch: mode: single + # interactive (default) = port-forward + upload + tail; batch = exec + image + no-wait. + # Can be overridden per-invocation with `--mode` on launch/run. + runMode: interactive + # Where the code lives: upload (default, Ray SDK), image (baked + # at launch.codePath), or lustre (mounted at launch.codePath). + codeSource: upload + codePath: null attach: generation: null gym: null diff --git a/tools/nrl_k8s/src/nrl_k8s/k8s.py b/tools/nrl_k8s/src/nrl_k8s/k8s.py index fe9023d2dfd..af4bf71581d 100644 --- a/tools/nrl_k8s/src/nrl_k8s/k8s.py +++ b/tools/nrl_k8s/src/nrl_k8s/k8s.py @@ -183,11 +183,37 @@ def delete_configmap(name: str, namespace: str, *, ignore_missing: bool = True) raise +def get_head_pod(cluster_name: str, namespace: str) -> Any: + """Return the first ``Running`` head pod for a RayCluster. + + Used by the exec submitter to pick a shell target. KubeRay labels every + head pod with ``ray.io/cluster=,ray.io/node-type=head`` — that + selector uniquely identifies a single pod today (KubeRay runs exactly + one head per cluster), but we still filter on phase to avoid returning + a ``Pending`` or ``Terminating`` instance from a mid-rollout restart. + """ + load_kubeconfig() + core = client.CoreV1Api() + selector = f"ray.io/cluster={cluster_name},ray.io/node-type=head" + resp = with_retries( + lambda: core.list_namespaced_pod(namespace=namespace, label_selector=selector) + ) + for pod in resp.items: + if pod.status and pod.status.phase == "Running": + return pod + raise RuntimeError( + f"no Running head pod found for RayCluster {cluster_name!r} in " + f"namespace {namespace!r} (label selector: {selector}). Check " + f"`kubectl -n {namespace} get pods -l {selector}`." + ) + + __all__ = [ "apply_raycluster", "custom_objects_api", "delete_configmap", "delete_raycluster", + "get_head_pod", "get_raycluster", "list_rayclusters", "load_kubeconfig", diff --git a/tools/nrl_k8s/src/nrl_k8s/orchestrate.py b/tools/nrl_k8s/src/nrl_k8s/orchestrate.py index 482b5c793dd..c94269dacbb 100644 --- a/tools/nrl_k8s/src/nrl_k8s/orchestrate.py +++ b/tools/nrl_k8s/src/nrl_k8s/orchestrate.py @@ -33,7 +33,8 @@ from . import k8s, submit, workdir from .config import LoadedConfig from .manifest import build_raycluster_manifest -from .schema import ClusterSpec, InfraConfig +from .schema import ClusterSpec, CodeSource, InfraConfig, SubmitterMode +from .submitters import SubmissionHandle, build_submitter, save_handle Role = Literal["generation", "gym", "training"] ALL_ROLES: tuple[Role, ...] = ("generation", "gym", "training") @@ -41,8 +42,17 @@ @dataclass class RunResult: - training_dashboard: str - training_job_id: str + """Outcome of a training submission. + + ``handle`` carries the transport-specific identifiers the observability + commands need. ``training_dashboard`` / ``training_job_id`` remain for + back-compat with earlier callers that printed Ray submission ids + directly; they are set to ``None``/``""`` for exec submissions. + """ + + handle: SubmissionHandle + training_dashboard: str | None = None + training_job_id: str = "" # ============================================================================= @@ -160,8 +170,26 @@ def submit_training( log: callable, repo_root: Path, replace: bool = False, + run_id: str | None = None, ) -> RunResult: - """Stage + submit the training job against the training cluster.""" + """Submit the training job against the training cluster. + + Dispatches on ``infra.submit.submitter`` + ``infra.launch.codeSource``: + + * ``submitter=portForward`` + ``codeSource=upload`` — today's path. + Stages a working_dir, opens port-forward, submits via Ray SDK. + * ``submitter=portForward`` + ``codeSource in (image, lustre)`` — no + staging; Ray job inherits the head pod's cwd. The entrypoint is + responsible for ``cd`` + ``source``. + * ``submitter=exec`` — ``kubectl exec`` into the head, run the user + entrypoint under ``nohup`` + ``disown``. No staging, no + port-forward. ``codeSource`` must not be ``upload`` on this path. + + ``run_id`` is used as the submission id / pidfile tag. Ray + port-forward submissions generate one if ``run_id`` is None (Ray's + default behaviour); exec submissions require a non-empty value and + caller is expected to synthesize one via :func:`default_run_id`. + """ infra = loaded.infra launch = infra.launch if not launch.entrypoint: @@ -173,19 +201,35 @@ def submit_training( cluster = _require_cluster(infra, "training") name = cluster.name - log("[training] staging working_dir ...") - recipe_yaml = OmegaConf.to_yaml(loaded.recipe) - wd = workdir.stage_workdir( - repo_root, - include_paths=_upload_paths(infra), - extra_files={"nrl_k8s_run.yaml": recipe_yaml}, - ) + submitter = build_submitter(infra) + is_exec = infra.submit.submitter is SubmitterMode.EXEC + upload = launch.codeSource is CodeSource.UPLOAD + + if is_exec and upload: + raise ValueError( + "infra.submit.submitter=exec is incompatible with " + "infra.launch.codeSource=upload — pick image or lustre, " + "or switch submitter to portForward." + ) + + # Stage only if we're actually uploading. Exec + image/lustre modes + # rely on the code being on the pod's filesystem already. + wd: Path | None = None + if upload: + log("[training] staging working_dir ...") + recipe_yaml = OmegaConf.to_yaml(loaded.recipe) + wd = workdir.stage_workdir( + repo_root, + include_paths=_upload_paths(infra), + extra_files={"nrl_k8s_run.yaml": recipe_yaml}, + ) - with submit.dashboard_url(name, infra.namespace) as dash: - # Training jobs have auto-generated submissionIds, so no ID collision; - # ``--replace`` just stops any RUNNING job on the cluster so the new - # one can claim GPUs. - if replace: + # `--replace` semantics: stop any running job on the training cluster + # so the new one can claim GPUs. Only applies to the Ray path; exec + # runs are keyed by run_id (unique per submission) so replace is a + # no-op there. + if replace and not is_exec: + with submit.dashboard_url(name, infra.namespace) as dash: client = JobSubmissionClient(dash) for job in client.list_jobs(): if job.status is JobStatus.RUNNING: @@ -200,15 +244,40 @@ def submit_training( except Exception as exc: # noqa: BLE001 log(f"[training] warning: stop failed: {exc}") - log(f"[training] submitting training job via {dash}") - job_id = submit.submit_ray_job( - dash, - entrypoint=launch.entrypoint, - working_dir=wd, - env_vars=launch.env, - ) - log(f"[training] training job submitted: {job_id}") - return RunResult(training_dashboard=dash, training_job_id=job_id) + if is_exec: + run_id = run_id or default_run_id("training") + log(f"[training] exec submitter: launching as run_id={run_id} on head pod") + else: + log("[training] port-forward submitter: submitting Ray Job") + + # Always expose the run id to the training entrypoint. Both transports + # see the same variable so recipe authors can reference + # ``$NRL_K8S_RUN_ID`` (in ``logger.wandb.name`` etc.) without caring + # which submitter the CLI picked. + env_vars = {**dict(launch.env)} + if run_id: + env_vars.setdefault("NRL_K8S_RUN_ID", run_id) + + handle = submitter.submit( + name, + infra.namespace, + entrypoint=launch.entrypoint, + run_id=run_id or "", + env_vars=env_vars, + working_dir=wd, + ) + save_handle(handle) + log(f"[training] training run handle: kind={handle.kind} id={handle.run_id}") + return RunResult( + handle=handle, + training_dashboard=None, # per-call port-forward, not persistent + training_job_id=handle.run_id, + ) + + +def default_run_id(role: str) -> str: + """Human-readable default id when the user didn't supply ``--run-id``.""" + return f"{role}-{int(time.time())}" def run( @@ -217,6 +286,7 @@ def run( log: callable, repo_root: Path, replace: bool = False, + run_id: str | None = None, ) -> RunResult: """Do the full sequence: bring up all 3 clusters + daemons, submit training.""" if replace: @@ -229,7 +299,9 @@ def run( name = bring_up_cluster(role, loaded, log=log) submit_daemon(role, loaded, name, log=log, repo_root=repo_root, replace=replace) - return submit_training(loaded, log=log, repo_root=repo_root, replace=replace) + return submit_training( + loaded, log=log, repo_root=repo_root, replace=replace, run_id=run_id + ) _JOB_ID_RE = re.compile(r"--job-id[= ]+(\S+)") @@ -328,8 +400,10 @@ def _wait_for_http(url: str, timeout_s: int, log: callable, role: Role) -> None: __all__ = [ + "ALL_ROLES", "RunResult", "bring_up_cluster", + "default_run_id", "run", "submit_daemon", "submit_training", diff --git a/tools/nrl_k8s/src/nrl_k8s/schema.py b/tools/nrl_k8s/src/nrl_k8s/schema.py index 597793289ae..34b782827c7 100644 --- a/tools/nrl_k8s/src/nrl_k8s/schema.py +++ b/tools/nrl_k8s/src/nrl_k8s/schema.py @@ -186,12 +186,35 @@ class DevPodMode(str, Enum): SKIP = "skip" +class SubmitterMode(str, Enum): + """How the CLI ships the training entrypoint onto the cluster. + + ``portForward`` (default) goes through Ray's Job SDK via a brief + ``kubectl port-forward`` to the head dashboard. The Ray job carries a + submission id the dashboard tracks. + + ``exec`` shells into the training head pod with ``kubectl exec`` and + launches the entrypoint as a backgrounded ``nohup`` process — the + same shape as a Slurm driver running on a login node. No Ray Job + abstraction; the process calls ``ray.init(address="auto")`` from + inside the head pod and joins the already-running cluster. + """ + + PORT_FORWARD = "portForward" + EXEC = "exec" + + class SubmitSpec(_StrictModel): kind: SubmitKind = SubmitKind.SDK portForward: PortForwardMode = PortForwardMode.AUTO devPod: DevPodMode = DevPodMode.AUTO # Local port when port-forwarding; default avoids collision with `kubectl-ray session`. localDashboardPort: int = 18265 + # Transport used to deliver the training entrypoint to the cluster. + submitter: SubmitterMode = SubmitterMode.PORT_FORWARD + # Pod directory ExecSubmitter uses for the launcher script, stdout log, + # and pidfile. Override if PodSecurityPolicy makes /tmp read-only. + execTmpDir: str = "/tmp" # ============================================================================= @@ -206,6 +229,46 @@ class LaunchMode(str, Enum): BRINGUP = "bringup" # create a long-lived RayCluster, no job +class RunMode(str, Enum): + """Interactive vs batch defaults for ``nrl-k8s run`` / ``launch``. + + ``interactive`` (default) preserves the dev-iteration UX: port-forward + submitter, ``codeSource=upload``, foreground log tailing, exits on + terminal state. + + ``batch`` flips production defaults: exec submitter, ``codeSource=image``, + no wait — the CLI returns a run id and exits as soon as ``nohup`` fires + so laptops are off the critical path for long-running training. + + Both defaults are macros — each individual dimension (submitter, + codeSource, no_wait) can still be overridden via explicit flag. + """ + + INTERACTIVE = "interactive" + BATCH = "batch" + + +class CodeSource(str, Enum): + """Where the training code lives at submission time. + + ``upload`` (default) stages a working_dir from the laptop and uploads it + via Ray's Job SDK. Subject to Ray's 100 MiB working_dir cap. + + ``image`` assumes the code is baked into the container image at + ``LaunchSpec.codePath`` (typically ``/opt/nemo-rl``). No CLI-side + staging, no upload. The entrypoint is responsible for ``cd``-ing there. + + ``lustre`` is structurally identical to ``image`` but implies the + ``codePath`` points at a Lustre / shared-FS mount populated by an + out-of-band snapshot step. Kept distinct so ``nrl-k8s check`` output + and future snapshot tooling can reason about it. + """ + + UPLOAD = "upload" + IMAGE = "image" + LUSTRE = "lustre" + + class AttachSpec(_StrictModel): generation: str | None = None gym: str | None = None @@ -214,13 +277,18 @@ class AttachSpec(_StrictModel): class LaunchSpec(_StrictModel): mode: LaunchMode = LaunchMode.SINGLE + # Interactive vs batch defaults — see :class:`RunMode`. Overridden by + # ``--mode`` on the CLI; unset on the infra layer means "interactive". + runMode: RunMode = RunMode.INTERACTIVE attach: AttachSpec = Field(default_factory=AttachSpec) peerWatcher: bool = True # Shell command the training job runs inside the Ray cluster. Required # for `nrl-k8s launch` / `nrl-k8s run`. Typically a line like # ``python -u examples/.../entry.py --config nrl_k8s_run.yaml ...``. # The CLI stages the resolved recipe as ``nrl_k8s_run.yaml`` at the - # working_dir root so this command can reference it by name. + # working_dir root so this command can reference it by name (only when + # ``codeSource=upload``; in image/lustre mode the entrypoint must use + # the in-container recipe path directly). entrypoint: str | None = None # Env vars injected into the training job's runtime_env. env: dict[str, str] = Field(default_factory=dict) @@ -228,7 +296,15 @@ class LaunchSpec(_StrictModel): # None means "use the built-in default" (see nrl_k8s.workdir). Keeping # this narrow matters — Ray caps working_dir uploads at 100 MiB, so # recipes should exclude datasets they don't need for the run. + # Only consulted when ``codeSource=upload``. rayUploadPaths: list[str] | None = None + # Where the training code lives — see :class:`CodeSource`. When + # ``image`` or ``lustre``, ``codePath`` must be set. + codeSource: CodeSource = CodeSource.UPLOAD + # Absolute path inside the container where the code lives when + # codeSource is image or lustre. The entrypoint is expected to ``cd`` + # there and (if applicable) ``source`` any env setup. + codePath: str | None = None @model_validator(mode="after") def _attach_fields(self) -> "LaunchSpec": @@ -240,6 +316,16 @@ def _attach_fields(self) -> "LaunchSpec": ) return self + @model_validator(mode="after") + def _code_path_required_for_non_upload(self) -> "LaunchSpec": + if self.codeSource in (CodeSource.IMAGE, CodeSource.LUSTRE) \ + and not self.codePath: + raise ValueError( + f"infra.launch.codePath is required when codeSource=" + f"{self.codeSource.value}" + ) + return self + # ============================================================================= # Resource profiles per role (CLI derives sensible defaults from cluster.*) @@ -374,6 +460,7 @@ def _not_blank(cls, v: str) -> str: "CheckpointsSpec", "ClusterSpec", "ClustersSpec", + "CodeSource", "DaemonSpec", "DevPodMode", "HFCacheKind", @@ -387,10 +474,12 @@ def _not_blank(cls, v: str) -> str: "PortForwardMode", "ResourceProfile", "ResourcesSpec", + "RunMode", "SchedulerKind", "SchedulerSpec", "SubmitKind", "SubmitSpec", + "SubmitterMode", "Toleration", "WorkspaceKind", "WorkspaceSpec", diff --git a/tools/nrl_k8s/src/nrl_k8s/submitters/__init__.py b/tools/nrl_k8s/src/nrl_k8s/submitters/__init__.py new file mode 100644 index 00000000000..d09e5f1e944 --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/submitters/__init__.py @@ -0,0 +1,154 @@ +# 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 +"""Training-job submitters. + +Two transports, same shape. The abstraction keeps the orchestrator and CLI +agnostic about whether a run lives under Ray's Job framework or as a raw +process on the head pod. + +* :class:`PortForwardSubmitter` opens a brief ``kubectl port-forward`` to + the head dashboard and uses Ray's ``JobSubmissionClient``. The Ray job + carries a submission id the dashboard tracks. Dev-iteration default. + +* :class:`ExecSubmitter` shells into the head pod with ``kubectl exec`` + and launches the entrypoint as a ``nohup`` process — the same shape as + a Slurm driver on a login node. Production / batch default. There is + no Ray Job abstraction on this path; training code calls + ``ray.init(address="auto")`` to attach to the already-running head. + +Handles are persisted to ``~/.cache/nrl-k8s/runs/.json`` so that +``nrl-k8s job logs`` / ``job stop`` on a later invocation knows which +transport to talk to. +""" + +from __future__ import annotations + +import json +import os +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import Any, Iterator, Literal, Protocol + +from ..schema import InfraConfig, SubmitterMode + +HandleKind = Literal["ray", "exec"] +JobStatusStr = Literal["running", "succeeded", "failed", "stopped", "unknown"] + + +@dataclass +class SubmissionHandle: + """Opaque reference to a submitted training run. + + Carries just enough to find the run later: which transport submitted + it, what cluster it's on, and transport-specific identifiers (Ray + submission id, or a head-pod / pidfile pair for exec). + """ + + kind: HandleKind + run_id: str + cluster_name: str + namespace: str + # exec-mode only + pod: str | None = None + tmp_dir: str | None = None + # ray-mode only + dashboard_url: str | None = None + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> "SubmissionHandle": + return cls(**d) + + +class JobSubmitter(Protocol): + """Transport abstraction for training-job submission + observability.""" + + def submit( + self, + cluster_name: str, + namespace: str, + *, + entrypoint: str, + run_id: str, + env_vars: dict[str, str] | None = None, + working_dir: Path | None = None, + ) -> SubmissionHandle: ... + + def follow(self, handle: SubmissionHandle) -> Iterator[str]: ... + + def status(self, handle: SubmissionHandle) -> JobStatusStr: ... + + def stop(self, handle: SubmissionHandle, *, force: bool = False) -> None: ... + + +# ============================================================================= +# Factory +# ============================================================================= + +def build_submitter(infra: InfraConfig) -> JobSubmitter: + """Pick a submitter based on the resolved infra config.""" + # Local imports to avoid cycles — the two submitters import back to get + # the dataclasses + Protocol defined above. + if infra.submit.submitter is SubmitterMode.EXEC: + from .exec_ import ExecSubmitter + return ExecSubmitter(exec_tmp_dir=infra.submit.execTmpDir) + from .portforward import PortForwardSubmitter + return PortForwardSubmitter() + + +# ============================================================================= +# Handle cache +# ============================================================================= + +def _cache_root() -> Path: + """Resolve the handle cache dir at call time. + + Reading the env var on every call (instead of once at import) keeps + tests simple — a fixture can ``monkeypatch.setenv`` and immediately + see the new location without a package reload. + """ + base = os.environ.get("NRL_K8S_CACHE_DIR") + if base: + return Path(base) / "runs" + return Path.home() / ".cache" / "nrl-k8s" / "runs" + + +def handle_path(run_id: str) -> Path: + return _cache_root() / f"{run_id}.json" + + +def save_handle(handle: SubmissionHandle) -> Path: + root = _cache_root() + root.mkdir(parents=True, exist_ok=True) + p = handle_path(handle.run_id) + p.write_text(json.dumps(handle.to_dict(), indent=2)) + return p + + +def load_handle(run_id: str) -> SubmissionHandle | None: + p = handle_path(run_id) + if not p.exists(): + return None + try: + return SubmissionHandle.from_dict(json.loads(p.read_text())) + except (json.JSONDecodeError, TypeError): + return None + + +__all__ = [ + "HandleKind", + "JobStatusStr", + "JobSubmitter", + "SubmissionHandle", + "build_submitter", + "handle_path", + "load_handle", + "save_handle", +] diff --git a/tools/nrl_k8s/src/nrl_k8s/submitters/exec_.py b/tools/nrl_k8s/src/nrl_k8s/submitters/exec_.py new file mode 100644 index 00000000000..e2378f46059 --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/submitters/exec_.py @@ -0,0 +1,374 @@ +# 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 +"""``kubectl exec`` submitter — runs the training entrypoint as a raw +backgrounded process on the training head pod. + +Same shape as a Slurm driver on a login node. The user's entrypoint +(``python -u run_grpo.py …``) is wrapped into a launcher script that +``source``s env vars the user declared, runs the command under +``nohup`` with redirected stdio, and captures ``$!`` as a pidfile + +``$?`` as an exitcode file. ``nrl-k8s job logs / stop`` then talk to +the same directory via additional ``kubectl exec`` calls. + +Why not ``ray job submit``? Two reasons: + +1. The training driver calls ``ray.init(address="auto")`` anyway; there + is no meaningful difference between "Ray Job" and "regular driver" — + the dashboard only adds a status row and a log buffer that duplicates + the file we're already writing. +2. Ray's ``runtime_env.env_vars`` doesn't merge with ``ray.init``'s + captured env — setting both is a recurring source of "Failed to + merge" crashes in prior versions of these examples. Using shell + ``export`` avoids the class entirely. +""" + +from __future__ import annotations + +import re +import shlex +import subprocess +import tempfile +import time +from pathlib import Path +from typing import Iterator + +from .. import k8s +from . import JobStatusStr, SubmissionHandle + + +_VALID_ID = re.compile(r"^[a-zA-Z0-9_.-]+$") +# Upper bound for how long we wait for the pidfile to appear on the pod +# after the launch exec fires. In normal conditions the pidfile lands in +# well under a second; a flaky control plane can push it to a few +# seconds. 30 s is generous headroom without blocking the caller for +# the full websocket teardown (which can be 60+ seconds on EKS). +_PIDFILE_POLL_SECONDS = 30 + + +class ExecSubmitter: + """Back-grounded-process transport.""" + + def __init__(self, *, exec_tmp_dir: str = "/tmp") -> None: + self._tmp_root = exec_tmp_dir.rstrip("/") or "/tmp" + + # ---------- submit -------------------------------------------------- + + def submit( + self, + cluster_name: str, + namespace: str, + *, + entrypoint: str, + run_id: str, + env_vars: dict[str, str] | None = None, + working_dir: Path | None = None, + ) -> SubmissionHandle: + if working_dir is not None: + # Exec submitter never uploads. Callers who want uploads should + # pick PortForwardSubmitter. + raise ValueError( + "ExecSubmitter does not support working_dir upload; set " + "infra.launch.codeSource to image or lustre (or switch " + "submitter to portForward)." + ) + + _validate_run_id(run_id) + pod = k8s.get_head_pod(cluster_name, namespace) + pod_name = pod.metadata.name + + tmp_dir = f"{self._tmp_root}/nrl-{run_id}" + entry_path = f"{tmp_dir}/entry.sh" + log_path = f"{tmp_dir}/stdout.log" + pid_path = f"{tmp_dir}/pid" + exitcode_path = f"{tmp_dir}/exitcode" + + launcher = _render_launcher( + run_id=run_id, + env_vars=env_vars or {}, + user_entrypoint=entrypoint, + exitcode_path=exitcode_path, + ) + + # mkdir + cp happen as two separate exec calls because `kubectl cp` + # uses `tar` under the hood which fails if the destination + # directory doesn't exist on the pod side. + _run(["kubectl", "exec", "-n", namespace, pod_name, "--", + "mkdir", "-p", tmp_dir]) + + with tempfile.NamedTemporaryFile("w", suffix=".sh", delete=False) as f: + f.write(launcher) + local_entry = Path(f.name) + try: + _run([ + "kubectl", "cp", + str(local_entry), + f"{namespace}/{pod_name}:{entry_path}", + ]) + finally: + local_entry.unlink(missing_ok=True) + + # The launcher is backgrounded with ``nohup`` + ``disown`` + + # redirected stdio + `` {shlex.quote(log_path)} 2>&1 {shlex.quote(pid_path)}; " + f"disown; " + f"exit 0" + ) + exec_cmd = ["kubectl", "exec", "-n", namespace, pod_name, "--", + "bash", "-c", bg] + exec_proc = subprocess.Popen( + exec_cmd, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + # Poll the pidfile directly — independent of the exec + # subprocess's lifecycle. A fresh ``kubectl exec cat`` call per + # poll is cheap (< 1 s) and has its own short-lived websocket, + # so the flaky long-lived one from the bg launch doesn't block + # anything. + deadline = time.monotonic() + _PIDFILE_POLL_SECONDS + pid = "" + while time.monotonic() < deadline: + try: + pid = _run( + ["kubectl", "exec", "-n", namespace, pod_name, "--", + "cat", pid_path], + capture=True, capture_stderr=True, + ).strip() + except _ExecFailed: + time.sleep(0.5) + continue + if pid.isdigit(): + break + time.sleep(0.5) + + # Either we have a pid (success) or we exhausted the budget + # (fail). Either way the bg-launch exec subprocess is no longer + # useful to us; kill it so we return promptly. + if exec_proc.poll() is None: + exec_proc.terminate() + try: + exec_proc.wait(timeout=1) + except subprocess.TimeoutExpired: + exec_proc.kill() + + if not pid.isdigit(): + stderr = "" + try: + _, stderr = exec_proc.communicate(timeout=1) + except subprocess.TimeoutExpired: + pass + raise RuntimeError( + f"exec submit: pidfile {pid_path} did not appear on " + f"{pod_name} within {_PIDFILE_POLL_SECONDS}s. Stderr " + f"from launch exec was:\n{stderr or ''}" + ) + + return SubmissionHandle( + kind="exec", + run_id=run_id, + cluster_name=cluster_name, + namespace=namespace, + pod=pod_name, + tmp_dir=tmp_dir, + ) + + # ---------- follow / status / stop --------------------------------- + + def follow(self, handle: SubmissionHandle) -> Iterator[str]: + tmp = _require_tmp(handle) + cmd = [ + "kubectl", "exec", "-n", handle.namespace, _require_pod(handle), "--", + "tail", "-F", "-n", "500", f"{tmp}/stdout.log", + ] + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + ) + assert proc.stdout is not None + try: + for line in iter(proc.stdout.readline, ""): + yield line + finally: + proc.terminate() + try: + proc.wait(timeout=2) + except subprocess.TimeoutExpired: + proc.kill() + + def status(self, handle: SubmissionHandle) -> JobStatusStr: + tmp = _require_tmp(handle) + pod = _require_pod(handle) + # `kill -0 $pid` returns 0 if the process exists. The exitcode file + # only appears after the launcher finishes; so: + # - kill -0 ok → running (exitcode file may or may not exist) + # - kill -0 fail + exitcode=0 → succeeded + # - kill -0 fail + exitcode!=0 → failed + # - kill -0 fail + no exitcode → stopped (killed externally / pod restarted) + probe = ( + f'if [ -f {shlex.quote(tmp)}/pid ] && kill -0 "$(cat {shlex.quote(tmp)}/pid)" 2>/dev/null; then ' + f' echo running; ' + f'elif [ -f {shlex.quote(tmp)}/exitcode ]; then ' + f' ec=$(cat {shlex.quote(tmp)}/exitcode); ' + f' if [ "$ec" = "0" ]; then echo succeeded; else echo failed; fi; ' + f'else ' + f' echo stopped; ' + f'fi' + ) + try: + out = _run( + ["kubectl", "exec", "-n", handle.namespace, pod, "--", "bash", "-c", probe], + capture=True, + ).strip() + except subprocess.CalledProcessError: + return "unknown" + return out if out in ("running", "succeeded", "failed", "stopped") else "unknown" + + def stop(self, handle: SubmissionHandle, *, force: bool = False) -> None: + tmp = _require_tmp(handle) + pod = _require_pod(handle) + sig = "KILL" if force else "TERM" + kill = ( + f'if [ -f {shlex.quote(tmp)}/pid ]; then ' + f' kill -s {sig} "$(cat {shlex.quote(tmp)}/pid)" 2>/dev/null || true; ' + f'fi' + ) + _run(["kubectl", "exec", "-n", handle.namespace, pod, "--", + "bash", "-c", kill]) + + +# ============================================================================= +# Internals +# ============================================================================= + +def _validate_run_id(run_id: str) -> None: + if not run_id: + raise ValueError("run_id is required for exec submissions") + if not _VALID_ID.match(run_id): + raise ValueError( + f"invalid run_id {run_id!r}: must match [a-zA-Z0-9_.-]+ " + "(used in a pod path)" + ) + + +def _render_launcher( + *, + run_id: str, + env_vars: dict[str, str], + user_entrypoint: str, + exitcode_path: str, +) -> str: + """Produce the bash script we kubectl-cp onto the head pod. + + The script ``export``s every declared env var, runs the user's + entrypoint, and writes the trailing exit code to a sentinel file so + ``status()`` can tell succeeded from killed-mid-run. + """ + lines = [ + "#!/bin/bash", + "# Generated by nrl-k8s ExecSubmitter — do not edit by hand.", + # `set -e` would make a nonzero exit in the user's entrypoint skip + # our exitcode writer; we don't want that. Just propagate the + # exit code explicitly. + "set -u", + "set -o pipefail", + f"export NRL_K8S_RUN_ID={shlex.quote(run_id)}", + ] + for k, v in env_vars.items(): + _validate_env_key(k) + lines.append(f"export {k}={shlex.quote(v)}") + lines.append("") + lines.append("# ---- user entrypoint (verbatim) ----") + lines.append(user_entrypoint.rstrip()) + lines.append("ec=$?") + lines.append(f"echo $ec > {shlex.quote(exitcode_path)}") + lines.append("exit $ec") + return "\n".join(lines) + "\n" + + +_ENV_KEY_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") + + +def _validate_env_key(k: str) -> None: + if not _ENV_KEY_RE.match(k): + raise ValueError(f"invalid env var name {k!r}") + + +def _require_pod(h: SubmissionHandle) -> str: + if not h.pod: + raise ValueError(f"exec handle for {h.run_id} is missing pod") + return h.pod + + +def _require_tmp(h: SubmissionHandle) -> str: + if not h.tmp_dir: + raise ValueError(f"exec handle for {h.run_id} is missing tmp_dir") + return h.tmp_dir + + +class _ExecFailed(Exception): + """Wraps a failed kubectl exec/cp with its stderr payload. + + Callers that want to reconcile the error against side-effects on the + pod (e.g. a pidfile written before the websocket dropped) can catch + this explicitly and re-raise only if the side-effect is missing. + """ + + def __init__(self, returncode: int, stderr: str) -> None: + super().__init__(f"kubectl exited {returncode}: {stderr}") + self.returncode = returncode + self.stderr = stderr + + +def _run(cmd: list[str], *, capture: bool = False, capture_stderr: bool = False) -> str: + """Thin ``subprocess.run`` wrapper. + + - ``capture=True`` returns stdout as str; stderr is inherited. + - ``capture_stderr=True`` captures stderr so transient "connection + reset" lines from kubectl don't pollute the CLI when the caller + has a stronger success signal (e.g. a pidfile on the pod). On a + non-zero exit we raise :class:`_ExecFailed` with the captured + stderr body. + - No stdin: ``-svc :8265`` +for the duration of each call, submits / tails / queries through Ray's +``JobSubmissionClient``, then tears the forward down. +""" + +from __future__ import annotations + +import re +import shlex +from pathlib import Path +from typing import Any, Iterator + +from .. import submit as _submit +from . import JobStatusStr, SubmissionHandle + + +_ENV_KEY_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") + + +class PortForwardSubmitter: + """Ray Job SDK transport, reached via a short-lived ``kubectl port-forward``. + + ``working_dir=None`` is supported — the Ray job inherits whatever cwd + the head pod's default entrypoint ran with, which is useful when + ``infra.launch.codeSource`` is ``image`` or ``lustre`` (code already + on disk inside the container) but the user still wants Ray's Job API + for tracking. + + Env vars are **inlined as shell ``export``s** in the entrypoint + rather than sent through ``runtime_env.env_vars``. The latter + conflicts with the captured ``os.environ`` that ``ray.init(address= + "auto")`` later hands to worker actors — Ray raises "Failed to merge + the Job's runtime env" when it sees the same key in both. Keeping + the transport's interface uniform with :class:`ExecSubmitter` + (env via ``export``, not a Ray-specific channel) avoids the whole + class. + """ + + def submit( + self, + cluster_name: str, + namespace: str, + *, + entrypoint: str, + run_id: str, + env_vars: dict[str, str] | None = None, + working_dir: Path | None = None, + ) -> SubmissionHandle: + from ray.job_submission import JobSubmissionClient + + wrapped_entrypoint = _prepend_env_exports(entrypoint, env_vars or {}) + + with _submit.dashboard_url(cluster_name, namespace) as dash: + runtime_env: dict[str, Any] = {} + if working_dir is not None: + runtime_env["working_dir"] = str(working_dir) + client = JobSubmissionClient(dash) + submission_id = client.submit_job( + entrypoint=wrapped_entrypoint, + runtime_env=runtime_env, + submission_id=run_id, + ) + return SubmissionHandle( + kind="ray", + run_id=submission_id, + cluster_name=cluster_name, + namespace=namespace, + dashboard_url=None, # port-forward doesn't survive the with-block + ) + + def follow(self, handle: SubmissionHandle) -> Iterator[str]: + with _submit.dashboard_url(handle.cluster_name, handle.namespace) as dash: + yield from _submit.tail_job_logs(dash, handle.run_id) + + def status(self, handle: SubmissionHandle) -> JobStatusStr: + from ray.job_submission import JobStatus, JobSubmissionClient + + with _submit.dashboard_url(handle.cluster_name, handle.namespace) as dash: + try: + state = JobSubmissionClient(dash).get_job_status(handle.run_id) + except Exception: # noqa: BLE001 + return "unknown" + return _RAY_STATUS_MAP.get(state, "unknown") + + def stop(self, handle: SubmissionHandle, *, force: bool = False) -> None: + # Ray's API has no concept of force-kill vs graceful — stop_job asks + # the head to SIGTERM the driver. We accept and ignore ``force``. + del force + from ray.job_submission import JobSubmissionClient + + with _submit.dashboard_url(handle.cluster_name, handle.namespace) as dash: + JobSubmissionClient(dash).stop_job(handle.run_id) + + +def _build_ray_status_map() -> dict[Any, JobStatusStr]: + from ray.job_submission import JobStatus + + return { + JobStatus.PENDING: "running", + JobStatus.RUNNING: "running", + JobStatus.SUCCEEDED: "succeeded", + JobStatus.FAILED: "failed", + JobStatus.STOPPED: "stopped", + } + + +class _LazyRayStatusMap: + """Defer importing ``ray`` until first use. + + Ray is a heavy import (~2 s of startup). The CLI has commands that + never touch a dashboard, so we don't want to pay that cost on every + invocation just because the module imported. + """ + + _cached: dict[Any, JobStatusStr] | None = None + + def get(self, key: Any, default: JobStatusStr) -> JobStatusStr: + if self._cached is None: + self._cached = _build_ray_status_map() + return self._cached.get(key, default) + + +_RAY_STATUS_MAP = _LazyRayStatusMap() + + +def _prepend_env_exports(entrypoint: str, env_vars: dict[str, str]) -> str: + """Prepend ``export KEY=VAL`` lines to the entrypoint body. + + Ray's submit_job entrypoint runs under /bin/dash by default, so we + avoid bash-only syntax — each export lives on its own line with + shell-quoted values. Invalid env keys raise immediately so a typo + doesn't produce subtle failures deep in the job. + """ + if not env_vars: + return entrypoint + lines = [] + for k, v in env_vars.items(): + if not _ENV_KEY_RE.match(k): + raise ValueError(f"invalid env var name {k!r}") + lines.append(f"export {k}={shlex.quote(v)}") + lines.append(entrypoint) + return "\n".join(lines) + + +__all__ = ["PortForwardSubmitter"] diff --git a/tools/nrl_k8s/tests/unit/test_cli.py b/tools/nrl_k8s/tests/unit/test_cli.py index a47058b14fb..a8e18332fe3 100644 --- a/tools/nrl_k8s/tests/unit/test_cli.py +++ b/tools/nrl_k8s/tests/unit/test_cli.py @@ -164,6 +164,96 @@ def test_both_sources_rejected(self, tmp_path) -> None: # cluster down # ============================================================================= +class TestClusterDashboard: + """`nrl-k8s cluster dashboard ` wraps port-forward + browser + open, with an optional symlink-fix pre-step. No recipe/infra needed + — the cluster name is a positional argument, namespace comes from + --namespace or the active kube context.""" + + @staticmethod + def _stub_env(monkeypatch, browser_opens, pf_started, fix_called, + *, pf_cls_args=None, ns="ns-ctx"): + class _FakePF: + def __init__(self, cluster_name, namespace, port): + if pf_cls_args is not None: + pf_cls_args.append((cluster_name, namespace, port)) + self._alive = False + + def start(self): + pf_started.append(True) + self._alive = False # exit loop immediately + + def alive(self): + return self._alive + + def stop(self): + pass + + monkeypatch.setattr("nrl_k8s.submit._PortForward", _FakePF) + monkeypatch.setattr("nrl_k8s.submit.is_in_cluster", lambda: True) + monkeypatch.setattr("nrl_k8s.config._infer_kube_namespace", lambda: ns) + monkeypatch.setattr( + "webbrowser.open", lambda url: browser_opens.append(url) + ) + monkeypatch.setattr( + "nrl_k8s.cli._reinstall_ray_if_symlinked", + lambda cluster, ns: fix_called.append([cluster, ns]), + ) + + def test_positional_name_uses_kube_context_namespace(self, monkeypatch): + browser_opens: list[str] = [] + pf_started: list[bool] = [] + fix_called: list[list[str]] = [] + pf_args: list[tuple[str, str, int]] = [] + self._stub_env( + monkeypatch, browser_opens, pf_started, fix_called, + pf_cls_args=pf_args, ns="nemo-rl-testing", + ) + + runner = CliRunner() + result = runner.invoke( + cli.main, ["cluster", "dashboard", "raycluster-foo"] + ) + assert result.exit_code == 0, result.output + assert pf_started == [True] + assert fix_called == [["raycluster-foo", "nemo-rl-testing"]] + assert pf_args == [("raycluster-foo", "nemo-rl-testing", 8265)] + assert browser_opens == ["http://localhost:8265"] + + def test_namespace_flag_overrides_context(self, monkeypatch): + browser_opens: list[str] = [] + pf_started: list[bool] = [] + fix_called: list[list[str]] = [] + pf_args: list[tuple[str, str, int]] = [] + self._stub_env( + monkeypatch, browser_opens, pf_started, fix_called, + pf_cls_args=pf_args, ns="wrong-ns", + ) + + runner = CliRunner() + result = runner.invoke( + cli.main, + ["cluster", "dashboard", "rc-x", "-n", "explicit-ns", "--no-open"], + ) + assert result.exit_code == 0, result.output + assert fix_called == [["rc-x", "explicit-ns"]] + assert pf_args == [("rc-x", "explicit-ns", 8265)] + assert browser_opens == [] + + def test_no_fix_skips_reinstall(self, monkeypatch): + browser_opens: list[str] = [] + pf_started: list[bool] = [] + fix_called: list[list[str]] = [] + self._stub_env(monkeypatch, browser_opens, pf_started, fix_called) + + runner = CliRunner() + result = runner.invoke( + cli.main, + ["cluster", "dashboard", "rc-y", "--no-fix", "--no-open"], + ) + assert result.exit_code == 0, result.output + assert fix_called == [] + class TestClusterDown: def test_errors_without_role_or_name(self, tmp_path, monkeypatch) -> None: @@ -174,3 +264,95 @@ def test_errors_without_role_or_name(self, tmp_path, monkeypatch) -> None: result = runner.invoke(cli.main, ["cluster", "down", str(recipe)]) assert result.exit_code == 2 assert "--role" in result.output or "--name" in result.output + + +# ============================================================================= +# --mode resolution (interactive vs batch) +# ============================================================================= + +class TestModeResolution: + def test_interactive_defaults(self) -> None: + from nrl_k8s.schema import CodeSource, RunMode, SubmitterMode + + mode, sub, code, no_wait = cli._resolve_mode_defaults( + cli_mode=None, + infra_mode=RunMode.INTERACTIVE, + cli_submitter=None, + cli_code_source=None, + cli_wait=None, + ) + assert mode is RunMode.INTERACTIVE + assert sub is SubmitterMode.PORT_FORWARD + assert code is CodeSource.UPLOAD + assert no_wait is False + + def test_batch_defaults(self) -> None: + from nrl_k8s.schema import CodeSource, RunMode, SubmitterMode + + mode, sub, code, no_wait = cli._resolve_mode_defaults( + cli_mode="batch", + infra_mode=RunMode.INTERACTIVE, + cli_submitter=None, + cli_code_source=None, + cli_wait=None, + ) + assert mode is RunMode.BATCH + assert sub is SubmitterMode.EXEC + assert code is CodeSource.IMAGE + assert no_wait is True + + def test_explicit_submitter_overrides_mode(self) -> None: + """`--mode batch --submitter portForward` keeps the Ray transport.""" + from nrl_k8s.schema import CodeSource, RunMode, SubmitterMode + + _, sub, code, no_wait = cli._resolve_mode_defaults( + cli_mode="batch", + infra_mode=RunMode.INTERACTIVE, + cli_submitter="portForward", + cli_code_source=None, + cli_wait=None, + ) + assert sub is SubmitterMode.PORT_FORWARD + # codeSource still follows the batch macro. + assert code is CodeSource.IMAGE + assert no_wait is True + + def test_explicit_code_source_overrides_mode(self) -> None: + from nrl_k8s.schema import CodeSource, RunMode + + _, _, code, _ = cli._resolve_mode_defaults( + cli_mode="batch", + infra_mode=RunMode.INTERACTIVE, + cli_submitter=None, + cli_code_source="lustre", + cli_wait=None, + ) + assert code is CodeSource.LUSTRE + + def test_wait_flag_overrides_mode_default(self) -> None: + """`--mode batch --wait` should keep exec + image but follow logs.""" + _, _, _, no_wait = cli._resolve_mode_defaults( + cli_mode="batch", + infra_mode=__import__("nrl_k8s.schema", fromlist=["RunMode"]).RunMode.INTERACTIVE, + cli_submitter=None, + cli_code_source=None, + cli_wait=True, + ) + assert no_wait is False + + def test_infra_run_mode_used_without_cli_flag(self) -> None: + """`runMode: batch` in the infra YAML flips defaults even when + --mode isn't on the command line.""" + from nrl_k8s.schema import CodeSource, RunMode, SubmitterMode + + mode, sub, code, no_wait = cli._resolve_mode_defaults( + cli_mode=None, + infra_mode=RunMode.BATCH, + cli_submitter=None, + cli_code_source=None, + cli_wait=None, + ) + assert mode is RunMode.BATCH + assert sub is SubmitterMode.EXEC + assert code is CodeSource.IMAGE + assert no_wait is True diff --git a/tools/nrl_k8s/tests/unit/test_schema.py b/tools/nrl_k8s/tests/unit/test_schema.py index 3d9aa442a19..b92d5e025d8 100644 --- a/tools/nrl_k8s/tests/unit/test_schema.py +++ b/tools/nrl_k8s/tests/unit/test_schema.py @@ -13,13 +13,17 @@ from nrl_k8s.schema import ( CheckpointsKind, CheckpointsSpec, + CodeSource, HFCacheKind, HFCacheSpec, InfraConfig, LaunchMode, LaunchSpec, + RunMode, SchedulerKind, SchedulerSpec, + SubmitSpec, + SubmitterMode, WorkspaceKind, WorkspaceSpec, ) @@ -173,6 +177,44 @@ def test_attach_with_generation_only_ok(self) -> None: assert spec.attach.generation == "rc-gen" assert spec.attach.training is None + def test_run_mode_defaults_to_interactive(self) -> None: + assert LaunchSpec().runMode is RunMode.INTERACTIVE + + def test_code_source_defaults_to_upload(self) -> None: + assert LaunchSpec().codeSource is CodeSource.UPLOAD + + def test_code_path_required_for_image(self) -> None: + with pytest.raises(ValidationError, match="codePath is required"): + LaunchSpec.model_validate({"codeSource": "image"}) + + def test_code_path_required_for_lustre(self) -> None: + with pytest.raises(ValidationError, match="codePath is required"): + LaunchSpec.model_validate({"codeSource": "lustre"}) + + def test_code_path_ok_with_image(self) -> None: + spec = LaunchSpec.model_validate( + {"codeSource": "image", "codePath": "/opt/nemo-rl"} + ) + assert spec.codeSource is CodeSource.IMAGE + assert spec.codePath == "/opt/nemo-rl" + + def test_code_path_not_required_for_upload(self) -> None: + spec = LaunchSpec.model_validate({"codeSource": "upload"}) + assert spec.codePath is None + + +class TestSubmitterMode: + def test_default_is_port_forward(self) -> None: + assert SubmitSpec().submitter is SubmitterMode.PORT_FORWARD + + def test_exec_tmp_dir_default(self) -> None: + assert SubmitSpec().execTmpDir == "/tmp" + + def test_exec_tmp_dir_override(self) -> None: + spec = SubmitSpec.model_validate({"submitter": "exec", "execTmpDir": "/workspace/tmp"}) + assert spec.submitter is SubmitterMode.EXEC + assert spec.execTmpDir == "/workspace/tmp" + # ============================================================================= # Placement / tolerations diff --git a/tools/nrl_k8s/tests/unit/test_submitters.py b/tools/nrl_k8s/tests/unit/test_submitters.py new file mode 100644 index 00000000000..a403f8edc75 --- /dev/null +++ b/tools/nrl_k8s/tests/unit/test_submitters.py @@ -0,0 +1,420 @@ +# 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 +"""Tests for :mod:`nrl_k8s.submitters`. + +Covers: + +* Handle cache round-trip. +* ``build_submitter`` dispatches on :class:`SubmitterMode`. +* :class:`ExecSubmitter` generates a launcher that ``nohup``s + ``disown``s + and writes a pidfile + exitcode sentinel. +* Env-var values survive shell quoting (spaces, dollars, single quotes). +* ``kubectl cp`` is invoked for the launcher (simpler contract than a + length-threshold branch). +* Head-pod-not-found surfaces a cluster-name/namespace-bearing error. +* ``follow`` uses ``tail -F`` (not ``-f``), preserving through log rotation. +""" + +from __future__ import annotations + +import json +import types +from pathlib import Path +from typing import Any + +import pytest + +from nrl_k8s.submitters import ( + SubmissionHandle, + build_submitter, + handle_path, + load_handle, + save_handle, +) + + +# ============================================================================= +# Fixtures +# ============================================================================= + +@pytest.fixture(autouse=True) +def _tmp_cache_dir(tmp_path, monkeypatch): + """Redirect ~/.cache/nrl-k8s/runs for each test. + + ``_cache_root()`` re-reads NRL_K8S_CACHE_DIR on every call, so setting + the env var is enough — no reload needed. + """ + monkeypatch.setenv("NRL_K8S_CACHE_DIR", str(tmp_path)) + yield + + +@pytest.fixture +def fake_head_pod(monkeypatch): + """Make ``k8s.get_head_pod`` return a stub pod without contacting the API.""" + pod = types.SimpleNamespace( + metadata=types.SimpleNamespace(name="ray-head-abc12"), + status=types.SimpleNamespace(phase="Running"), + ) + monkeypatch.setattr("nrl_k8s.k8s.get_head_pod", lambda *a, **kw: pod) + return pod + + +# ============================================================================= +# SubmissionHandle + cache +# ============================================================================= + +class TestSubmissionHandle: + def test_roundtrip_ray(self, tmp_path): + h = SubmissionHandle( + kind="ray", + run_id="raysubmit_abc", + cluster_name="rc-train", + namespace="ns", + ) + p = save_handle(h) + assert p.exists() + loaded = load_handle("raysubmit_abc") + assert loaded == h + + def test_roundtrip_exec(self, tmp_path): + h = SubmissionHandle( + kind="exec", + run_id="train-1699999999", + cluster_name="rc-train", + namespace="ns", + pod="ray-head-xyz", + tmp_dir="/tmp/nrl-train-1699999999", + ) + save_handle(h) + loaded = load_handle("train-1699999999") + assert loaded == h + + def test_missing_handle_returns_none(self, tmp_path): + assert load_handle("never-submitted") is None + + +# ============================================================================= +# Factory +# ============================================================================= + +class TestBuildSubmitter: + def test_port_forward_default(self): + from nrl_k8s.schema import InfraConfig + + infra = InfraConfig.model_validate( + {"namespace": "ns", "image": "img:tag"} + ) + sub = build_submitter(infra) + from nrl_k8s.submitters.portforward import PortForwardSubmitter + + assert isinstance(sub, PortForwardSubmitter) + + def test_exec_when_selected(self): + from nrl_k8s.schema import InfraConfig + + infra = InfraConfig.model_validate( + { + "namespace": "ns", + "image": "img:tag", + "submit": {"submitter": "exec", "execTmpDir": "/scratch"}, + } + ) + sub = build_submitter(infra) + from nrl_k8s.submitters.exec_ import ExecSubmitter + + assert isinstance(sub, ExecSubmitter) + # /scratch, not /tmp — execTmpDir plumbed through. + assert sub._tmp_root == "/scratch" + + +# ============================================================================= +# ExecSubmitter — launcher script composition +# ============================================================================= + +def _capture_runs(monkeypatch): + """Replace the helpers that shell out so we can inspect the cmdlines. + + - ``_run`` covers mkdir + kubectl cp + pidfile-cat probes. + - ``subprocess.Popen`` covers the detached launch exec. + + Fake ``kubectl cp`` snarfs the launcher content from the local + tempfile so tests can assert on its body. Fake ``cat `` + returns a believable PID so the poll loop in submit() completes + on the first try. + """ + calls: list[list[str]] = [] + captured: dict[str, str] = {} + popen_calls: list[list[str]] = [] + + def fake_run(cmd, *, capture=False, capture_stderr=False): + calls.append(list(cmd)) + if len(cmd) >= 4 and cmd[0] == "kubectl" and cmd[1] == "cp": + local = Path(cmd[2]) + if local.exists(): + captured["launcher"] = local.read_text() + captured["dest"] = cmd[3] + # pidfile-cat probe -> return a PID so the poll loop succeeds. + if len(cmd) >= 7 and cmd[0] == "kubectl" and cmd[1] == "exec" \ + and cmd[-2] == "cat": + return "12345\n" + return "" + + class FakePopen: + def __init__(self, cmd, **kwargs): + popen_calls.append(list(cmd)) + # Return code doesn't matter — submit() only reads + # ``poll()`` to decide whether to terminate. Mark the + # subprocess as having exited cleanly so neither terminate() + # nor kill() need to run. + self._rc = 0 + + def poll(self): + return self._rc + + def terminate(self): + pass + + def wait(self, timeout=None): + return self._rc + + def kill(self): + pass + + def communicate(self, timeout=None): + return ("", "") + + monkeypatch.setattr("nrl_k8s.submitters.exec_._run", fake_run) + monkeypatch.setattr("nrl_k8s.submitters.exec_.subprocess.Popen", FakePopen) + captured["popen_calls"] = popen_calls + return calls, captured + + +class TestExecSubmitterLauncher: + def test_submit_invokes_mkdir_cp_exec(self, fake_head_pod, monkeypatch): + calls, captured = _capture_runs(monkeypatch) + from nrl_k8s.submitters.exec_ import ExecSubmitter + + ExecSubmitter(exec_tmp_dir="/tmp").submit( + "rc-train", "ns", + entrypoint="python run_grpo.py", + run_id="rid-1", + env_vars={"FOO": "bar"}, + ) + + # _run calls: mkdir exec + kubectl cp + (one or more) cat pidfile. + assert any(c[:2] == ["kubectl", "exec"] and "mkdir" in c for c in calls) + assert any(c[:2] == ["kubectl", "cp"] for c in calls) + # The detached launch goes through Popen, not _run. + assert any("nohup" in " ".join(c) for c in captured["popen_calls"]) + + def test_launcher_contains_nohup_disown_and_pid( + self, fake_head_pod, monkeypatch + ): + calls, captured = _capture_runs(monkeypatch) + from nrl_k8s.submitters.exec_ import ExecSubmitter + + ExecSubmitter(exec_tmp_dir="/tmp").submit( + "rc-train", "ns", + entrypoint="python run_grpo.py", + run_id="rid-2", + ) + + # The detached launch lives under Popen. + assert captured["popen_calls"], "no Popen invocation captured" + bg_cmd = captured["popen_calls"][0] + assert bg_cmd[:4] == ["kubectl", "exec", "-n", "ns"] + assert bg_cmd[5:7] == ["--", "bash"] + joined = " ".join(bg_cmd) + assert "nohup" in joined + assert "disown" in joined + assert " /tmp/nrl-rid-4/exitcode" in launcher + assert launcher.rstrip().endswith("exit $ec") + + def test_working_dir_rejected_on_exec(self, fake_head_pod, monkeypatch): + _capture_runs(monkeypatch) + from nrl_k8s.submitters.exec_ import ExecSubmitter + + with pytest.raises(ValueError, match="working_dir upload"): + ExecSubmitter().submit( + "rc-train", "ns", + entrypoint="echo", + run_id="rid-5", + working_dir=Path("/some/dir"), + ) + + def test_invalid_run_id_rejected(self, fake_head_pod, monkeypatch): + _capture_runs(monkeypatch) + from nrl_k8s.submitters.exec_ import ExecSubmitter + + with pytest.raises(ValueError, match="run_id"): + ExecSubmitter().submit( + "rc-train", "ns", + entrypoint="echo", + run_id="has spaces", + ) + + def test_returned_handle_populated(self, fake_head_pod, monkeypatch): + _capture_runs(monkeypatch) + from nrl_k8s.submitters.exec_ import ExecSubmitter + + handle = ExecSubmitter(exec_tmp_dir="/scratch").submit( + "rc-train", "ns", + entrypoint="echo", + run_id="rid-7", + ) + assert handle.kind == "exec" + assert handle.run_id == "rid-7" + assert handle.cluster_name == "rc-train" + assert handle.namespace == "ns" + assert handle.pod == "ray-head-abc12" + assert handle.tmp_dir == "/scratch/nrl-rid-7" + + +# ============================================================================= +# ExecSubmitter.follow / status / stop +# ============================================================================= + +class TestExecSubmitterObservability: + def test_follow_uses_tail_F(self, monkeypatch): + """tail -F (capital) survives log rotation; tail -f does not.""" + from nrl_k8s.submitters.exec_ import ExecSubmitter + + captured_cmd: list[list[str]] = [] + + import io + + class FakePopen: + def __init__(self, cmd, **kwargs): + captured_cmd.append(list(cmd)) + # Empty stream — `readline()` returns "" immediately, which + # terminates the iter() in follow(). + self.stdout = io.StringIO("") + + def terminate(self): + pass + + def wait(self, timeout=None): + return 0 + + def kill(self): + pass + + monkeypatch.setattr("nrl_k8s.submitters.exec_.subprocess.Popen", FakePopen) + + handle = SubmissionHandle( + kind="exec", run_id="r", cluster_name="rc", namespace="ns", + pod="head-1", tmp_dir="/tmp/nrl-r", + ) + list(ExecSubmitter().follow(handle)) + cmd = captured_cmd[0] + assert "tail" in cmd + assert "-F" in cmd + assert "-f" not in cmd + assert "/tmp/nrl-r/stdout.log" in cmd + + def test_status_dispatches_on_probe_output(self, monkeypatch): + from nrl_k8s.submitters.exec_ import ExecSubmitter + + for probe_out, expected in [ + ("running", "running"), + ("succeeded", "succeeded"), + ("failed", "failed"), + ("stopped", "stopped"), + ("garbage", "unknown"), + ]: + monkeypatch.setattr( + "nrl_k8s.submitters.exec_._run", + lambda cmd, *, capture=False, out=probe_out: out + "\n", + ) + handle = SubmissionHandle( + kind="exec", run_id="r", cluster_name="rc", namespace="ns", + pod="head-1", tmp_dir="/tmp/nrl-r", + ) + assert ExecSubmitter().status(handle) == expected + + def test_stop_signals_term_then_kill(self, monkeypatch): + from nrl_k8s.submitters.exec_ import ExecSubmitter + + calls: list[list[str]] = [] + monkeypatch.setattr( + "nrl_k8s.submitters.exec_._run", + lambda cmd, *, capture=False: calls.append(list(cmd)) or "", + ) + handle = SubmissionHandle( + kind="exec", run_id="r", cluster_name="rc", namespace="ns", + pod="head-1", tmp_dir="/tmp/nrl-r", + ) + ExecSubmitter().stop(handle) + ExecSubmitter().stop(handle, force=True) + # Both calls should be kubectl exec, and the second should pass KILL. + assert "kill -s TERM" in " ".join(calls[0]) + assert "kill -s KILL" in " ".join(calls[1]) + + +# ============================================================================= +# Head-pod lookup failure +# ============================================================================= + +class TestHeadPodLookup: + def test_no_running_head_pod_surfaces_cluster_and_ns(self, monkeypatch): + from nrl_k8s import k8s + + def raise_(*a, **kw): + raise RuntimeError( + "no Running head pod found for RayCluster 'rc-train' in " + "namespace 'ns'" + ) + + monkeypatch.setattr(k8s, "get_head_pod", raise_) + + from nrl_k8s.submitters.exec_ import ExecSubmitter + + with pytest.raises(RuntimeError) as excinfo: + ExecSubmitter().submit( + "rc-train", "ns", entrypoint="echo", run_id="r", + ) + assert "rc-train" in str(excinfo.value) + assert "ns" in str(excinfo.value) From 35622a45a0a1f25e828ff8763bf431ce23da85e0 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Tue, 21 Apr 2026 20:08:54 -0700 Subject: [PATCH 27/84] =?UTF-8?q?refactor:=20remove=20gpu-operator=20from?= =?UTF-8?q?=20helmfile=20=E2=80=94=20managed=20by=20NKX=20on=20prod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GPU Operator is pre-installed by NKX on production clusters, so we should not deploy our own. Rename prod gpu_backend to existing-nkx-gpu-operator, remove the gpu-operator helm release and values file, and update SETUP.md accordingly. Signed-off-by: Terry Kong --- infra/examples/monolithic-jobset.yaml | 321 ++++++++++++++++++++ infra/helm/helmfile.yaml | 18 +- infra/helm/values/gpu-operator.yaml | 4 - infra/helm/values/nvidia-device-plugin.yaml | 6 +- infra/kind/SETUP.md | 9 +- 5 files changed, 327 insertions(+), 31 deletions(-) create mode 100644 infra/examples/monolithic-jobset.yaml delete mode 100644 infra/helm/values/gpu-operator.yaml diff --git a/infra/examples/monolithic-jobset.yaml b/infra/examples/monolithic-jobset.yaml new file mode 100644 index 00000000000..a65e73fefb3 --- /dev/null +++ b/infra/examples/monolithic-jobset.yaml @@ -0,0 +1,321 @@ +# Monolithic RL JobSet for GB300 — single Ray cluster, colocated generation. +# +# Three ReplicatedJobs: +# head — Ray head daemon (CPU-only) +# workers — GPU worker pods (4 GPUs each), topology-aware via compute-domain + RoCE +# driver — Submits training job via ray job submit, gates JobSet success +# +# Prerequisites: +# kubectl apply -f kai-queue.yaml +# kubectl apply -f endpoint-registry-rbac.yaml +# +# Usage: +# kubectl apply -f monolithic-jobset.yaml +# kubectl get jobset monolithic-job -w +# +# Cleanup: +# kubectl delete jobset monolithic-job +# kubectl delete computedomain compute-domain-monolithic +# kubectl delete resourceclaimtemplate roce-monolithic +# # Note: the compute-domain ResourceClaimTemplate is auto-created by the +# # controller and will be cleaned up when the ComputeDomain CR is deleted. +# +# --- +# Topology-aware scheduling (DRA) +# +# Raw JobSets require you to create DRA resources yourself. Higher-level +# abstractions like Kubeflow TrainJob + TrainingRuntime auto-create them per job, +# but with raw JobSets you manage them explicitly. +# +# Each job needs its own set so its pods are co-located together (not mixed +# with another job's pods). Create a unique set per job and clean them up after. +# +# There are two things to create: +# +# 1. ComputeDomain CR (resource.nvidia.com/v1beta1): +# Ensures all worker pods land in the same NVSwitch/MNNVL compute domain. +# Set spec.channel.resourceClaimTemplate.name to a name of your choice. +# The compute-domain controller will AUTO-CREATE a ResourceClaimTemplate with +# that name, populated with the correct domainID (the ComputeDomain's UID), +# labels, and finalizer. Do NOT pre-create the ResourceClaimTemplate yourself +# — if one already exists with that name, the controller will skip creation +# and the template will be missing the domainID, breaking topology placement. +# Set numNodes to 0 for elastic mode (recommended). +# +# 2. ResourceClaimTemplate for RoCE NICs: +# Allocates RoCE (RDMA) NICs per worker pod for inter-node communication. +# This is a separate DRA driver (dra.networking.k8s.aws) — you create it yourself. +# Adjust the count based on your node type: +# - GB300 (p6e-gb300r.36xlarge): count: 8 (8 RoCE NICs per node) +# +# Only GPU worker pods need these claims (referenced via resources.claims in the +# container spec and resourceClaims in the pod spec). + +# --- 1. ComputeDomain (controller auto-creates the ResourceClaimTemplate) --- +apiVersion: resource.nvidia.com/v1beta1 +kind: ComputeDomain +metadata: + name: compute-domain-monolithic +spec: + channel: + resourceClaimTemplate: + name: compute-domain-monolithic # controller creates RCT with this name + numNodes: 0 # elastic mode — controller doesn't wait for a fixed node count +--- +# --- 2. RoCE ResourceClaimTemplate (manually created) --- +apiVersion: resource.k8s.io/v1 +kind: ResourceClaimTemplate +metadata: + name: roce-monolithic +spec: + spec: + devices: + requests: + - exactly: + count: 8 # 8 RoCE NICs per GB300 node; adjust for other node types + deviceClassName: roce.networking.k8s.aws + name: roce +--- +# --- JobSet --- +apiVersion: jobset.x-k8s.io/v1alpha2 +kind: JobSet +metadata: + name: monolithic-job + # labels: + # kai.scheduler/queue: default-queue +spec: + network: + enableDNSHostnames: true + publishNotReadyAddresses: true + + coordinator: + replicatedJob: head + jobIndex: 0 + podIndex: 0 + + successPolicy: + operator: All + targetReplicatedJobs: [driver] + + failurePolicy: + maxRestarts: 0 + rules: + - name: head_crash + action: FailJobSet + targetReplicatedJobs: [head] + - name: driver_crash + action: FailJobSet + targetReplicatedJobs: [driver] + - name: worker_crash + action: FailJobSet + targetReplicatedJobs: [workers] + + replicatedJobs: + # ======================== + # Ray head (CPU-only) + # ======================== + - name: head + replicas: 1 + template: + spec: + backoffLimit: 0 + completions: 1 + parallelism: 1 + template: + spec: + # schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + tolerations: + - operator: Exists + containers: + - name: ray-head + image: nvcr.io/nvidian/nemo-rl:nightly + command: ["/bin/bash", "-c"] + args: + - | + ulimit -n 65536 + ray start --head --port=6379 \ + --dashboard-host=0.0.0.0 \ + --num-gpus=0 \ + --object-store-memory=200000000 \ + --block + ports: + - { containerPort: 6379, name: gcs-server } + - { containerPort: 8265, name: dashboard } + - { containerPort: 10001, name: client } + readinessProbe: + exec: + command: ["ray", "health-check"] + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 5 + resources: + requests: { cpu: "2", memory: "8Gi" } + limits: { cpu: "8", memory: "32Gi" } + env: + - { name: RAY_ADDRESS, value: "127.0.0.1:6379" } + - { name: RAY_memory_monitor_refresh_ms, value: "0" } + volumeMounts: + - { name: dshm, mountPath: /dev/shm } + - { name: rl-workspace, mountPath: /mnt/rl-workspace } + volumes: + - name: dshm + emptyDir: { medium: Memory, sizeLimit: 16Gi } + - name: rl-workspace + persistentVolumeClaim: { claimName: rl-workspace } + + # ======================== + # GPU workers (4 pods × 4 GPUs = 16 GPUs) + # ======================== + - name: workers + replicas: 1 + template: + spec: + backoffLimit: 0 + completions: 4 + parallelism: 4 + template: + spec: + # schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + tolerations: + - operator: Exists + initContainers: + - name: wait-for-head + image: nvcr.io/nvidian/nemo-rl:nightly + command: ["/bin/bash", "-c"] + args: + - | + HEAD=monolithic-job-head-0-0.monolithic-job + echo "Waiting for head at ${HEAD}:6379..." + until ray health-check --address ${HEAD}:6379 >/dev/null 2>&1; do + sleep 5 + done + echo "Head is ready." + resources: + requests: { cpu: "200m", memory: "256Mi" } + containers: + - name: ray-worker + image: nvcr.io/nvidian/nemo-rl:nightly + command: ["/bin/bash", "-c"] + args: + - | + ulimit -n 65536 + HEAD=monolithic-job-head-0-0.monolithic-job + ray start --address=${HEAD}:6379 \ + --num-gpus=4 \ + --object-store-memory=200000000 \ + --block + resources: + claims: + - name: compute-domain-channel + - name: roce-channel + requests: + cpu: "32" + memory: "200Gi" + nvidia.com/gpu: "4" + limits: + cpu: "128" + memory: "800Gi" + nvidia.com/gpu: "4" + env: + - { name: RAY_ADDRESS, value: "monolithic-job-head-0-0.monolithic-job:6379" } + - { name: RAY_memory_monitor_refresh_ms, value: "0" } + - { name: HF_HOME, value: /mnt/rl-workspace/shared/hf-cache } + - { name: HF_DATASETS_CACHE, value: /mnt/rl-workspace/shared/hf-cache/datasets } + - { name: TRANSFORMERS_CACHE, value: /mnt/rl-workspace/shared/hf-cache } + - { name: NCCL_DEBUG, value: INFO } + - { name: NCCL_MNNVL_ENABLE, value: "1" } + volumeMounts: + - { name: dshm, mountPath: /dev/shm } + - { name: rl-workspace, mountPath: /mnt/rl-workspace } + securityContext: + capabilities: + add: [IPC_LOCK] + resourceClaims: + - name: compute-domain-channel + resourceClaimTemplateName: compute-domain-monolithic + - name: roce-channel + resourceClaimTemplateName: roce-monolithic + volumes: + - name: dshm + emptyDir: { medium: Memory, sizeLimit: 64Gi } + - name: rl-workspace + persistentVolumeClaim: { claimName: rl-workspace } + + # ======================== + # Driver (submits SFT training, gates success) + # ======================== + - name: driver + replicas: 1 + template: + spec: + backoffLimit: 0 + completions: 1 + parallelism: 1 + template: + spec: + # schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + tolerations: + - operator: Exists + initContainers: + - name: wait-for-head + image: nvcr.io/nvidian/nemo-rl:nightly + command: ["/bin/bash", "-c"] + args: + - | + HEAD=monolithic-job-head-0-0.monolithic-job + echo "Waiting for head at ${HEAD}:6379..." + until ray health-check --address ${HEAD}:6379 >/dev/null 2>&1; do + sleep 5 + done + echo "Head is ready." + resources: + requests: { cpu: "200m", memory: "256Mi" } + containers: + - name: driver + image: nvcr.io/nvidian/nemo-rl:nightly + command: ["/bin/bash", "-c"] + args: + - | + set -eo pipefail + HEAD=monolithic-job-head-0-0.monolithic-job + SUBMISSION_ID="sft-$(date +%s)" + + ray job submit --address http://${HEAD}:8265 \ + --submission-id ${SUBMISSION_ID} \ + -- bash -c " + cd /opt/nemo-rl + export HF_HOME=/mnt/rl-workspace/shared/hf-cache + export HF_DATASETS_CACHE=/mnt/rl-workspace/shared/hf-cache/datasets + export TRANSFORMERS_CACHE=/mnt/rl-workspace/shared/hf-cache + + python -u examples/run_sft.py \ + --config examples/configs/sft.yaml \ + policy.model_name=Qwen/Qwen3-0.6B \ + cluster.gpus_per_node=4 \ + cluster.num_nodes=4 \ + policy.train_global_batch_size=32 \ + policy.train_micro_batch_size=1 \ + policy.max_total_sequence_length=1024 \ + policy.dtensor_cfg.tensor_parallel_size=1 \ + sft.max_num_steps=10 \ + sft.val_period=5 \ + checkpointing.enabled=false \ + logger.wandb_enabled=false \ + logger.tensorboard_enabled=false + " 2>&1 + resources: + requests: { cpu: "500m", memory: "2Gi" } + limits: { cpu: "2", memory: "8Gi" } + env: + - { name: RAY_ADDRESS, value: "monolithic-job-head-0-0.monolithic-job:6379" } + volumeMounts: + - { name: rl-workspace, mountPath: /mnt/rl-workspace } + volumes: + - name: rl-workspace + persistentVolumeClaim: { claimName: rl-workspace } diff --git a/infra/helm/helmfile.yaml b/infra/helm/helmfile.yaml index 502885c100c..c36a26ca1e8 100644 --- a/infra/helm/helmfile.yaml +++ b/infra/helm/helmfile.yaml @@ -4,15 +4,13 @@ environments: - gpu_backend: device-plugin # nvkind handles toolkit/runtime, only need device plugin prod: values: - - gpu_backend: gpu-operator # full operator manages driver, toolkit, device plugin, NFD + - gpu_backend: existing-nkx-gpu-operator # managed by NKX, not by us --- repositories: - name: nvdp url: https://nvidia.github.io/k8s-device-plugin -- name: nvidia - url: https://helm.ngc.nvidia.com/nvidia - name: kuberay url: https://ray-project.github.io/kuberay-helm/ @@ -30,20 +28,6 @@ releases: - values/nvidia-device-plugin.yaml {{ end }} -# -# GPU: full operator (prod) — manages driver, toolkit, device plugin, NFD, DCGM -# -{{ if eq .Environment.Values.gpu_backend "gpu-operator" }} -- name: gpu-operator - namespace: gpu-operator - createNamespace: true - chart: nvidia/gpu-operator - wait: true - waitForJobs: true - values: - - values/gpu-operator.yaml -{{ end }} - # # KAI Scheduler: gang scheduling + fairshare for GPU workloads # diff --git a/infra/helm/values/gpu-operator.yaml b/infra/helm/values/gpu-operator.yaml deleted file mode 100644 index 962fe5145f8..00000000000 --- a/infra/helm/values/gpu-operator.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# GPU Operator values for production clusters. -# Set driver.enabled=false if the host already has the NVIDIA driver installed. -driver: - enabled: true diff --git a/infra/helm/values/nvidia-device-plugin.yaml b/infra/helm/values/nvidia-device-plugin.yaml index e2c13125d4d..4902e2b4ef7 100644 --- a/infra/helm/values/nvidia-device-plugin.yaml +++ b/infra/helm/values/nvidia-device-plugin.yaml @@ -1,8 +1,7 @@ -# NOTE: These overrides are kind-specific. On a real cluster with the GPU Operator, -# you would NOT use this file at all — the GPU Operator deploys its own device plugin. +# NOTE: These overrides are kind-specific. On a real cluster the GPU Operator +# deploys its own device plugin, so this file is not used. # Override default affinity which requires NFD labels (not available in kind). -# On a real cluster, NFD is deployed by the GPU Operator, so the default affinity works. affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: @@ -14,5 +13,4 @@ affinity: - linux # The device plugin pod needs the nvidia runtime to access NVML for GPU discovery. -# On a real cluster, the GPU Operator handles this automatically. runtimeClassName: nvidia diff --git a/infra/kind/SETUP.md b/infra/kind/SETUP.md index 8a472d2dbc8..f5ec037e7d4 100644 --- a/infra/kind/SETUP.md +++ b/infra/kind/SETUP.md @@ -81,9 +81,7 @@ helmfile -e prod sync kubectl apply -f infra/examples/kai-queue-prod.yaml ``` -This installs the full **GPU Operator** (instead of just the device plugin) along with KAI scheduler, KubeRay, and JobSet. The GPU Operator manages the NVIDIA driver, container toolkit, device plugin, NFD, and DCGM exporter. - -Set `driver.enabled=false` in `values/gpu-operator.yaml` if the cluster nodes already have the NVIDIA driver installed. +This installs KAI scheduler, KubeRay, and JobSet. The cluster is expected to already have the GPU Operator (or equivalent GPU provisioning) installed. ## Architecture @@ -176,7 +174,6 @@ infra/ │ ├── helmfile.yaml # environments: kind, prod │ └── values/ │ ├── nvidia-device-plugin.yaml # kind only -│ ├── gpu-operator.yaml # prod only │ ├── kai-scheduler.yaml │ └── kuberay-operator.yaml ├── examples/ # Workload examples @@ -194,7 +191,7 @@ infra/ | Environment | GPU component | Use case | |-------------|---------------|----------| | `kind` | nvidia-device-plugin | Local dev — nvkind handles toolkit/runtime | -| `prod` | gpu-operator (full) | Real clusters — operator manages everything | +| `prod` | (none — cluster provides GPU Operator) | Real clusters | Both environments include KAI scheduler, KubeRay operator, and JobSet controller. @@ -207,7 +204,7 @@ kind delete cluster --name nemo-rl ## Notes - **nvkind vs vanilla kind**: nvkind automates GPU device injection, nvidia-container-toolkit installation inside nodes, containerd nvidia runtime configuration, and RuntimeClass registration. -- **nvidia-device-plugin** (kind only): The full GPU Operator fails in kind because its driver validation doesn't work inside kind nodes. The lightweight device plugin with CDI discovery is sufficient since nvkind handles the runtime setup. +- **nvidia-device-plugin** (kind only): The GPU Operator doesn't work in kind because its driver validation fails inside kind nodes. The lightweight device plugin with CDI discovery is sufficient since nvkind handles the runtime setup. - **KAI scheduler** creates PodGroups automatically for recognized workload types (RayCluster, Job, PyTorchJob, JobSet, etc.). For bare pods, create a PodGroup manually and annotate with `pod-group-name`. ## Fairshare scheduling From 23fc1243502baed26e7923924129fd1399badd9c Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Tue, 21 Apr 2026 20:09:08 -0700 Subject: [PATCH 28/84] fix: KAI scheduler binder OOM and CDI runtimeClassName injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Increase binder memory from 200Mi to 1Gi — the default OOMs on large clusters (78+ nodes), which breaks DRA resource claim allocation. Set admission.gpuPodRuntimeClassName to "" to prevent the KAI admission webhook from injecting runtimeClassName: nvidia onto GPU pods. GPU Operator v25.10.0+ with cdi.enabled=true installs a nvidia RuntimeClass that triggers the management.nvidia.com CDI path, which fails with "unresolvable CDI devices" on clusters where CDI spec files are not fully configured. Requires KAI >= v0.13.0 (PR #1035). Signed-off-by: Terry Kong --- infra/helm/values/kai-scheduler.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/infra/helm/values/kai-scheduler.yaml b/infra/helm/values/kai-scheduler.yaml index b1ef917c182..c33125b081c 100644 --- a/infra/helm/values/kai-scheduler.yaml +++ b/infra/helm/values/kai-scheduler.yaml @@ -1,3 +1,22 @@ # KAI Scheduler configuration. defaultQueue: createDefaultQueue: true + +# Binder handles pod-to-node binding and DRA resource claim allocation. +# Default 200Mi is too low for large clusters (78+ nodes); OOMKill breaks DRA. +binder: + resources: + requests: + cpu: 100m + memory: 512Mi + limits: + cpu: 500m + memory: 1Gi + +# GPU Operator v25.10.0+ with cdi.enabled=true installs a "nvidia" RuntimeClass +# that triggers the management.nvidia.com CDI path. By default KAI's admission +# webhook injects runtimeClassName: nvidia onto every GPU pod, which breaks on +# clusters where CDI spec files aren't fully configured. Setting this to "" stops +# the injection (requires KAI >= v0.13.0, PR #1035). +admission: + gpuPodRuntimeClassName: "" From feb3183c067c099ab68fb78428f04de54a839517 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Tue, 21 Apr 2026 20:09:22 -0700 Subject: [PATCH 29/84] feat: monolithic jobset examples for GB300 Add two JobSet variants for running monolithic (colocated) training on GB300 nodes with DRA topology-aware scheduling: - monolithic-jobset.yaml: uses KAI scheduler with queue support - monolithic-jobset-no-kai.yaml: uses default scheduler (for comparing DRA behavior between schedulers) Both use ComputeDomain + RoCE ResourceClaimTemplates for topology-aware placement and run SFT with Qwen3-0.6B as a smoke test. Signed-off-by: Terry Kong --- infra/examples/monolithic-jobset-no-kai.yaml | 276 +++++++++++++++++++ infra/examples/monolithic-jobset.yaml | 10 +- 2 files changed, 281 insertions(+), 5 deletions(-) create mode 100644 infra/examples/monolithic-jobset-no-kai.yaml diff --git a/infra/examples/monolithic-jobset-no-kai.yaml b/infra/examples/monolithic-jobset-no-kai.yaml new file mode 100644 index 00000000000..ee1ef138c1a --- /dev/null +++ b/infra/examples/monolithic-jobset-no-kai.yaml @@ -0,0 +1,276 @@ +# Same as monolithic-jobset.yaml but WITHOUT KAI scheduler. +# Uses default-scheduler so your colleague can diff the two and compare +# pod specs, resource claim allocation, and CDI device injection. +# +# The only differences from monolithic-jobset.yaml: +# - No kai.scheduler/queue label +# - No schedulerName: kai-scheduler on pod specs +# - Resource names suffixed with "-no-kai" to avoid conflicts +# +# Usage: +# kubectl apply -f monolithic-jobset-no-kai.yaml +# kubectl get jobset monolithic-job-no-kai -w + +# --- 1. ComputeDomain (controller auto-creates the ResourceClaimTemplate) --- +apiVersion: resource.nvidia.com/v1beta1 +kind: ComputeDomain +metadata: + name: compute-domain-monolithic-no-kai +spec: + channel: + resourceClaimTemplate: + name: compute-domain-monolithic-no-kai + numNodes: 0 +--- +# --- 2. RoCE ResourceClaimTemplate (manually created) --- +apiVersion: resource.k8s.io/v1 +kind: ResourceClaimTemplate +metadata: + name: roce-monolithic-no-kai +spec: + spec: + devices: + requests: + - exactly: + count: 8 + deviceClassName: roce.networking.k8s.aws + name: roce +--- +# --- JobSet --- +apiVersion: jobset.x-k8s.io/v1alpha2 +kind: JobSet +metadata: + name: monolithic-job-no-kai +spec: + network: + enableDNSHostnames: true + publishNotReadyAddresses: true + + coordinator: + replicatedJob: head + jobIndex: 0 + podIndex: 0 + + successPolicy: + operator: All + targetReplicatedJobs: [driver] + + failurePolicy: + maxRestarts: 0 + rules: + - name: head_crash + action: FailJobSet + targetReplicatedJobs: [head] + - name: driver_crash + action: FailJobSet + targetReplicatedJobs: [driver] + - name: worker_crash + action: FailJobSet + targetReplicatedJobs: [workers] + + replicatedJobs: + # ======================== + # Ray head (CPU-only) + # ======================== + - name: head + replicas: 1 + template: + spec: + backoffLimit: 0 + completions: 1 + parallelism: 1 + template: + spec: + imagePullSecrets: + - name: nvcr-secret + tolerations: + - operator: Exists + containers: + - name: ray-head + image: nvcr.io/nvidian/nemo-rl:nightly + command: ["/bin/bash", "-c"] + args: + - | + ulimit -n 65536 + ray start --head --port=6379 \ + --dashboard-host=0.0.0.0 \ + --num-gpus=0 \ + --object-store-memory=200000000 \ + --block + ports: + - { containerPort: 6379, name: gcs-server } + - { containerPort: 8265, name: dashboard } + - { containerPort: 10001, name: client } + readinessProbe: + exec: + command: ["ray", "health-check"] + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 5 + resources: + requests: { cpu: "2", memory: "8Gi" } + limits: { cpu: "8", memory: "32Gi" } + env: + - { name: RAY_ADDRESS, value: "127.0.0.1:6379" } + - { name: RAY_memory_monitor_refresh_ms, value: "0" } + volumeMounts: + - { name: dshm, mountPath: /dev/shm } + - { name: rl-workspace, mountPath: /mnt/rl-workspace } + volumes: + - name: dshm + emptyDir: { medium: Memory, sizeLimit: 16Gi } + - name: rl-workspace + persistentVolumeClaim: { claimName: rl-workspace } + + # ======================== + # GPU workers (4 pods × 4 GPUs = 16 GPUs) + # ======================== + - name: workers + replicas: 1 + template: + spec: + backoffLimit: 0 + completions: 4 + parallelism: 4 + template: + spec: + imagePullSecrets: + - name: nvcr-secret + tolerations: + - operator: Exists + initContainers: + - name: wait-for-head + image: nvcr.io/nvidian/nemo-rl:nightly + command: ["/bin/bash", "-c"] + args: + - | + HEAD=monolithic-job-no-kai-head-0-0.monolithic-job-no-kai + echo "Waiting for head at ${HEAD}:6379..." + until ray health-check --address ${HEAD}:6379 >/dev/null 2>&1; do + sleep 5 + done + echo "Head is ready." + resources: + requests: { cpu: "200m", memory: "256Mi" } + containers: + - name: ray-worker + image: nvcr.io/nvidian/nemo-rl:nightly + command: ["/bin/bash", "-c"] + args: + - | + ulimit -n 65536 + HEAD=monolithic-job-no-kai-head-0-0.monolithic-job-no-kai + ray start --address=${HEAD}:6379 \ + --num-gpus=4 \ + --object-store-memory=200000000 \ + --block + resources: + claims: + - name: compute-domain-channel + - name: roce-channel + requests: + cpu: "32" + memory: "200Gi" + nvidia.com/gpu: "4" + limits: + cpu: "128" + memory: "800Gi" + nvidia.com/gpu: "4" + env: + - { name: RAY_ADDRESS, value: "monolithic-job-no-kai-head-0-0.monolithic-job-no-kai:6379" } + - { name: RAY_memory_monitor_refresh_ms, value: "0" } + - { name: HF_HOME, value: /mnt/rl-workspace/shared/hf-cache } + - { name: HF_DATASETS_CACHE, value: /mnt/rl-workspace/shared/hf-cache/datasets } + - { name: TRANSFORMERS_CACHE, value: /mnt/rl-workspace/shared/hf-cache } + - { name: NCCL_DEBUG, value: INFO } + - { name: NCCL_MNNVL_ENABLE, value: "1" } + volumeMounts: + - { name: dshm, mountPath: /dev/shm } + - { name: rl-workspace, mountPath: /mnt/rl-workspace } + securityContext: + capabilities: + add: [IPC_LOCK] + resourceClaims: + - name: compute-domain-channel + resourceClaimTemplateName: compute-domain-monolithic-no-kai + - name: roce-channel + resourceClaimTemplateName: roce-monolithic-no-kai + volumes: + - name: dshm + emptyDir: { medium: Memory, sizeLimit: 64Gi } + - name: rl-workspace + persistentVolumeClaim: { claimName: rl-workspace } + + # ======================== + # Driver (submits SFT training, gates success) + # ======================== + - name: driver + replicas: 1 + template: + spec: + backoffLimit: 0 + completions: 1 + parallelism: 1 + template: + spec: + imagePullSecrets: + - name: nvcr-secret + tolerations: + - operator: Exists + initContainers: + - name: wait-for-head + image: nvcr.io/nvidian/nemo-rl:nightly + command: ["/bin/bash", "-c"] + args: + - | + HEAD=monolithic-job-no-kai-head-0-0.monolithic-job-no-kai + echo "Waiting for head at ${HEAD}:6379..." + until ray health-check --address ${HEAD}:6379 >/dev/null 2>&1; do + sleep 5 + done + echo "Head is ready." + resources: + requests: { cpu: "200m", memory: "256Mi" } + containers: + - name: driver + image: nvcr.io/nvidian/nemo-rl:nightly + command: ["/bin/bash", "-c"] + args: + - | + set -eo pipefail + HEAD=monolithic-job-no-kai-head-0-0.monolithic-job-no-kai + SUBMISSION_ID="sft-$(date +%s)" + + ray job submit --address http://${HEAD}:8265 \ + --submission-id ${SUBMISSION_ID} \ + -- bash -c " + cd /opt/nemo-rl + export HF_HOME=/mnt/rl-workspace/shared/hf-cache + export HF_DATASETS_CACHE=/mnt/rl-workspace/shared/hf-cache/datasets + export TRANSFORMERS_CACHE=/mnt/rl-workspace/shared/hf-cache + + python -u examples/run_sft.py \ + --config examples/configs/sft.yaml \ + policy.model_name=Qwen/Qwen3-0.6B \ + cluster.gpus_per_node=4 \ + cluster.num_nodes=4 \ + policy.train_global_batch_size=32 \ + policy.train_micro_batch_size=1 \ + policy.max_total_sequence_length=1024 \ + policy.dtensor_cfg.tensor_parallel_size=1 \ + sft.max_num_steps=10 \ + sft.val_period=5 \ + checkpointing.enabled=false \ + logger.wandb_enabled=false \ + logger.tensorboard_enabled=false + " 2>&1 + resources: + requests: { cpu: "500m", memory: "2Gi" } + limits: { cpu: "2", memory: "8Gi" } + env: + - { name: RAY_ADDRESS, value: "monolithic-job-no-kai-head-0-0.monolithic-job-no-kai:6379" } + volumeMounts: + - { name: rl-workspace, mountPath: /mnt/rl-workspace } + volumes: + - name: rl-workspace + persistentVolumeClaim: { claimName: rl-workspace } diff --git a/infra/examples/monolithic-jobset.yaml b/infra/examples/monolithic-jobset.yaml index a65e73fefb3..d8c8e15875e 100644 --- a/infra/examples/monolithic-jobset.yaml +++ b/infra/examples/monolithic-jobset.yaml @@ -81,8 +81,8 @@ apiVersion: jobset.x-k8s.io/v1alpha2 kind: JobSet metadata: name: monolithic-job - # labels: - # kai.scheduler/queue: default-queue + labels: + kai.scheduler/queue: default-queue spec: network: enableDNSHostnames: true @@ -123,7 +123,7 @@ spec: parallelism: 1 template: spec: - # schedulerName: kai-scheduler + schedulerName: kai-scheduler imagePullSecrets: - name: nvcr-secret tolerations: @@ -177,7 +177,7 @@ spec: parallelism: 4 template: spec: - # schedulerName: kai-scheduler + schedulerName: kai-scheduler imagePullSecrets: - name: nvcr-secret tolerations: @@ -257,7 +257,7 @@ spec: parallelism: 1 template: spec: - # schedulerName: kai-scheduler + schedulerName: kai-scheduler imagePullSecrets: - name: nvcr-secret tolerations: From 7b05950afca607e09083d71a00f515f3c784786b Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Tue, 21 Apr 2026 20:09:35 -0700 Subject: [PATCH 30/84] docs: kubernetes onboarding guide with aws-cmh and nemo-ci-h100 Add step-by-step onboarding for the aws-cmh (GB300) and nemo-ci-h100 EKS clusters, covering access requests, nvsec/AWS SSO auth, kubeconfig setup, shared PVC workspace, and running jobs with nrl-k8s. Signed-off-by: Terry Kong --- kubernetes-onboarding.md | 489 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 489 insertions(+) create mode 100644 kubernetes-onboarding.md diff --git a/kubernetes-onboarding.md b/kubernetes-onboarding.md new file mode 100644 index 00000000000..2ba46358c57 --- /dev/null +++ b/kubernetes-onboarding.md @@ -0,0 +1,489 @@ +# Kubernetes Onboarding + +## Prerequisites + +### Install kubectl + +```bash +# macOS +brew install kubectl + +# Linux (amd64) +curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" +chmod +x kubectl && sudo mv kubectl /usr/local/bin/ +``` + +### Install kubectx and kubens + +`kubectx` switches between clusters; `kubens` switches the default namespace +for the current context. + +```bash +# macOS +brew install kubectx + +# Linux +sudo apt install kubectx +# or from source: +git clone https://github.com/ahmetb/kubectx ~/.kubectx +ln -s ~/.kubectx/kubectx ~/.local/bin/kubectx +ln -s ~/.kubectx/kubens ~/.local/bin/kubens +``` + +### Install nrl-k8s + +The `nrl-k8s` CLI launches NeMo-RL training jobs on Kubernetes. It lives +under `tools/nrl_k8s/` and will be available on `main` soon. Until then, +install from the `hemil/k8s-infra-cp` branch: + +```bash +uv tool install "nrl-k8s @ git+https://github.com/NVIDIA-NeMo/RL.git@hemil/k8s-infra-cp#subdirectory=tools/nrl_k8s" +nrl-k8s --version +``` + +If you have the repo checked out locally, you can install from whatever +branch you're on: + +```bash +uv tool install ./tools/nrl_k8s +``` + +In both cases, reinstall to pick up changes: + +```bash +uv tool install --reinstall ./tools/nrl_k8s +``` + +### Install AWS CLI v2 + +Required for EKS clusters (nemo-ci-h100, aws-cmh). + +```bash +# macOS +brew install awscli + +# Linux +curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" +unzip awscliv2.zip && sudo ./aws/install +``` + +--- + +## 1. aws-cmh (GB300) + +### 1.1 Requesting access + +Request access to the AWS account via one of the following DLs: + +- **Admins:** [access-aws-nemo-rl-dev-admin](https://dlrequest/GroupID/Groups/Properties?identity=MzI2NjViN2ViMjdkNGQ0ZGEwMWYxYjhiMmMzN2E0NGJ8Z3JvdXA=) +- **Users:** [access-aws-nemo-rl-dev-engineer](https://dlrequest/GroupID/Groups/Properties?identity=MDVmMzI0OTc5ZDhmNDk5ZWI3MjlkY2E1ZjAxOWVkMDZ8Z3JvdXA=) + +> **Note:** After your DL request is approved, permissions sync on the hour, +> so you may need to wait up to an hour before you can log in. + +### 1.2 Logging in + +Authenticate with AWS using `nvsec`: + +```bash +nvsec aws auth +``` + +If you're on a remote machine (e.g. SSH'd into a devbox), use the +no-browser flag: + +```bash +nvsec aws auth --no-browser +``` + +This will print a URL like: + +``` +https://awscloud.nvidia.com/cli-login?redirect_uri=http://localhost:53682/callback&state=...&client=nvsec +``` + +Note the port number in the URL (e.g. `53682`). Before opening the URL, +set up a port forward from your local machine so the callback can reach +the remote host: + +```bash +ssh -L 53682:localhost:53682 +``` + +Then open the URL in your local browser. The auth should complete in your +terminal. + +### 1.3 Selecting the AWS profile + +List available accounts: + +```bash +nvsec aws list +``` + +You should see `nemo-rl-dev` listed: + +``` +Available roles (3): + + nemo-rl-dev + Account: 942195279341 | MPA: DGX_CLOUD_MPA + 0) CS-Admin + 1) CS-Engineer-942195279341 + + NeMo_Megatron + Account: 766267172432 | MPA: DGX_CLOUD_MPA + 2) CS-Admin +``` + +Configure credentials for your access level. When prompted for a profile +name, press Enter to accept the default: + +```bash +nvsec aws configure 0 # CS-Admin +nvsec aws configure 1 # CS-Engineer +``` + +### 1.4 Setting up your kubeconfig + +Add the EKS cluster to your kubeconfig: + +```bash +aws eks update-kubeconfig \ + --name ltqlfcnzyr-dgxc-k8s-aws-use2-prod \ + --region us-east-2 +``` + +The command creates a context with a long ARN name. Create a friendly alias: + +```bash +kubectx aws-cmh=arn:aws:eks:us-east-2:942195279341:cluster/ltqlfcnzyr-dgxc-k8s-aws-use2-prod +``` + +Switch to the context: + +```bash +kubectx aws-cmh +``` + +Verify access: + +```bash +kubectl get nodes # should list a bunch of nodes +kubectl auth can-i create rayclusters # should print "yes" +``` + +### 1.5 Shared workspace (PVC) + +The `default` namespace has a shared FSx Lustre PVC called `rl-workspace` +(1.2 TiB, ReadWriteMany). All pods across all nodes can read and write to +it simultaneously. + +The PVC already exists — you don't need to create it. If it ever needs to +be recreated: + +```bash +kubectl apply -f - <<'EOF' +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: rl-workspace + namespace: default +spec: + accessModes: + - ReadWriteMany + storageClassName: dgxc-enterprise-file + resources: + requests: + storage: 1200Gi +EOF +``` + +> **Note:** FSx Lustre has a minimum size of 1.2 TiB. Provisioning takes +> 5–15 minutes while AWS creates the Lustre filesystem. Watch progress with: +> +> ```bash +> kubectl get pvc rl-workspace -w +> ``` +> +> It will show `Pending` until the filesystem is ready, then flip to `Bound`. +> If you see `ProvisioningFailed` / `DeadlineExceeded` events, that's normal +> — the CSI driver retries automatically. + +Verify: + +```bash +kubectl get pvc rl-workspace # should show Bound, 1200Gi, RWX, dgxc-enterprise-file +``` + +To resize (only works after the PVC is `Bound` — if you try while +`Pending`, you'll get): + +``` +The PersistentVolumeClaim "rl-workspace" is invalid: spec: Forbidden: spec is immutable after creation except resources.requests and volumeAttributesClassName for bound claims +``` + +Once bound, resize with: + +```bash +kubectl patch pvc rl-workspace -p '{"spec":{"resources":{"requests":{"storage":"2400Gi"}}}}' +``` + +FSx Lustre grows in increments of 2.4 TiB. + +This PVC is shared across the team. To avoid collisions, organize by +username: + +``` +/mnt/rl-workspace/ +├── terryk/ +│ ├── data/ +│ ├── checkpoints/ +│ └── hf-cache/ +├── hemild/ +│ └── ... +└── shared/ + └── models/ +``` + +You can create additional PVCs if needed, but data cannot be shared across +different PVCs. To deduplicate things like HuggingFace model downloads, it's +easier to start with this shared PVC and only create a separate one if you +need isolation. + +The PVC uses `reclaimPolicy: Retain`, so data persists even if pods are +deleted. Do not delete the PVC itself — it's shared across the team. + +## 2. nemo-ci-h100 + +### 2.1 Requesting access + +TODO + +### 2.2 Logging in + +Authenticate with AWS SSO using the `megatron` profile: + +```bash +aws sso login --profile megatron +``` + +This will attempt to open your browser. If you're on a remote machine (e.g. +SSH'd into a devbox), the browser won't open and you'll see something like: + +``` +$ aws sso login --profile megatron +Attempting to open your default browser. If the browser does not open, open the following URL. +If you are unable to open the URL on this device, run this command again with the '--use-device-code' option. + +https://oidc.us-east-2.amazonaws.com/authorize?response_type=code&client_id=...&redirect_uri=http%3A%2F%2F127.0.0.1%3A33977%2Foauth%2Fcallback&state=... +``` + +Open that URL in your local browser. It will redirect to a `127.0.0.1` callback +URL, which will fail because the redirect targets the remote machine. Note the +port number in the redirect URL (e.g. `33977`) and set up a port forward from +your local machine: + +```bash +ssh -L 33977:localhost:33977 +``` + +Then refresh the page in your browser. You should see an AWS page that says: + +> Your credentials have been shared successfully and can be used until your +> session expires. You can now close this tab. + +Back in your terminal, the login will complete: + +``` +Successfully logged into Start URL: https://nv-h100.awsapps.com/start +``` + +Verify the session is active: + +```bash +aws sts get-caller-identity --profile megatron +``` + +You should see something like: + +```json +{ + "UserId": "AROA3E2IVEZIKPBBBR34J:terryk@nvidia.com", + "Account": "766267172432", + "Arn": "arn:aws:sts::766267172432:assumed-role/AWSReservedSSO_CS-Admin_a7cbef6db22f1b0b/terryk@nvidia.com" +} +``` + +You'll need to re-run `aws sso login` whenever your SSO token expires +(typically every 8–12 hours). + +### 2.3 Setting up your kubeconfig + +Add the EKS cluster to your kubeconfig and create a friendly context name: + +```bash +aws eks update-kubeconfig \ + --region us-east-1 \ + --name geyydnzzhv-dgxc-k8s-aws-use1-prod \ + --profile megatron \ + --alias nemo-ci-h100 +``` + +Switch to the context and set the default namespace: + +```bash +kubectx nemo-ci-h100 +kubens nemo-rl-testing +``` + +Verify access: + +```bash +kubectl get nodes # should list a bunch of nodes +kubectl auth can-i create rayclusters # should print "yes" +``` + +### 2.4 Shared workspace (PVC) + +The `nemo-rl-testing` namespace has a shared EFS PVC called `rl-workspace` +(100 GiB, ReadWriteMany). It's backed by AWS EFS so all pods across all +nodes can read and write to it simultaneously. + +The PVC already exists — you don't need to create it. If it ever needs to +be recreated: + +```bash +kubectl apply -f - <<'EOF' +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: rl-workspace + namespace: nemo-rl-testing +spec: + accessModes: + - ReadWriteMany + storageClassName: efs-rwx + resources: + requests: + storage: 100Gi +EOF +``` + +Verify: + +```bash +kubectl get pvc rl-workspace # should show Bound, 100Gi, RWX, efs-rwx +``` + +Use this PVC for training data, model caches, checkpoints, and code. To +avoid collisions between users, organize by username: + +``` +/mnt/rl-workspace/ +├── terryk/ +│ ├── data/ # training datasets +│ ├── checkpoints/ # training checkpoints +│ └── hf-cache/ # HuggingFace model cache +├── hemild/ +│ └── ... +└── shared/ + └── models/ # models everyone uses +``` + +To get a shell on the head node and set up your data: + +```bash +kubectl exec -it -- bash + +# Inside the pod: +mkdir -p /mnt/rl-workspace/$(whoami)/{data,checkpoints,hf-cache} +``` + +The PVC uses `reclaimPolicy: Retain`, so data persists even if pods are +deleted. Do not delete the PVC itself — it's shared across the team. + +### 2.5 Running a monolithic RayCluster + +This deploys a single RayCluster with training, colocated vLLM generation, +and gym all on one node. + +Validate the config: + +```bash +nrl-k8s check \ + tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml +``` + +Bring up the cluster: + +```bash +nrl-k8s cluster up \ + tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ + --role training +``` + +Submit training and tail logs. This uploads your local code (the paths +listed in `rayUploadPaths` in the infra YAML) to the Ray cluster as a +working directory, then runs the entrypoint on the cluster: + +```bash +nrl-k8s launch \ + tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ + --follow +``` + +Or do everything in one step (cluster up + job submit): + +```bash +nrl-k8s run \ + tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ + --follow +``` + +Check status: + +```bash +nrl-k8s status \ + tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml +``` + +List and inspect jobs: + +```bash +nrl-k8s job list \ + tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ + --role training + +nrl-k8s job logs \ + tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ + --role training +``` + +Tear down when done: + +```bash +nrl-k8s cluster down \ + tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ + --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ + --role training +``` + +## 3. kind + +TODO + +## 4. Cursor setup + +TODO + +## 5. Claude Code setup + +TODO From 8fea8d501df6a69523717f9c915fef9365d02f0e Mon Sep 17 00:00:00 2001 From: Hemil Desai Date: Wed, 22 Apr 2026 05:16:54 -0700 Subject: [PATCH 31/84] infra(nrl-k8s): GB300 single-cluster examples + 500-step recipe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit p6e-gb300r.36xlarge advertises 4 GPUs per node (not 8), so the single- cluster recipe now declares cluster.gpus_per_node=4 on both training and colocated generation — otherwise the worker stays Pending forever on this hardware. max_num_steps bumped from 200 → 500 for a longer dev run. Two new infra files target GB300 EKS (no EFA device plugin, no node taints, arm64): * qwen3_4b_if_single.gb300.infra.yaml — dev-mode (portForward upload) mirroring the base example but sized for ~140 CPU / 925 GiB nodes. * qwen3_4b_if_single.gb300.prod.infra.yaml — prod-mode (portForward + codeSource=image, codePath=/opt/nemo-rl). Mounts the shared FSx Lustre PVC rl-workspace at /opt/nemo-rl (subPath=hemild/rl-k8s) and at /mnt/rl-workspace. Entrypoint points DISAGG_TRAIN/VALID_PATH at a snapshot of nvidia/Nemotron-RL-instruction_following staged on Lustre, so edits on Lustre take effect without rebuilding the image. Secrets are pulled via secretKeyRef (wandb-api-key), nothing embedded. Signed-off-by: Hemil Desai Signed-off-by: Terry Kong --- .../qwen3_4b_if_single.gb300.infra.yaml | 147 +++++++++++++++ .../qwen3_4b_if_single.gb300.prod.infra.yaml | 170 ++++++++++++++++++ .../nrl_k8s/examples/qwen3_4b_if_single.yaml | 6 +- 3 files changed, 320 insertions(+), 3 deletions(-) create mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml create mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml new file mode 100644 index 00000000000..62724afc95c --- /dev/null +++ b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml @@ -0,0 +1,147 @@ +# Infra for qwen3_4b_if_single.yaml on a GB300 (p6e-gb300r.36xlarge) EKS cluster. +# +# Per-node hardware: 4× NVIDIA-GB300, ~140 CPU, ~925 GiB memory, arm64. GB300 +# nodes advertise no taints on this cluster — plain nodeSelector is enough. +# There is no aws-efa-k8s-device-plugin here, so vpc.amazonaws.com/efa is not +# a schedulable resource; NCCL/UCX ride the Mellanox NICs over hostNetwork. +# +# Sizing mirrors the paired recipe (1 node × 4 GPUs). Namespace is inferred +# from the current kube context (default — the FSx Lustre PVC `rl-workspace` +# is namespace-scoped and lives there, so experiments run in `default`). + +_shared: + nodeSelector: &shared_node_selector + nvidia.com/gpu.product: NVIDIA-GB300 + headEnv: &shared_head_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + workerEnv: &shared_worker_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + headResources: &shared_head_resources + limits: {cpu: "8", memory: "32Gi"} + requests: {cpu: "2", memory: "8Gi"} + gpuWorkerResources: &gpu_worker_resources + # p6e-gb300r.36xlarge allocatable: ~139.6 CPU, ~924 GiB, 4 GPUs. + # Claim 128 CPU / 880 GiB to leave headroom for the Ray head pod + + # gpu-operator / device-plugin / dgxc daemonsets (~11 CPU, ~40 GiB). + limits: + cpu: "128" + memory: "880Gi" + nvidia.com/gpu: "4" + requests: + cpu: "128" + memory: "880Gi" + nvidia.com/gpu: "4" + gpuHeadPorts: &gpu_head_ports + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + dshmMounts: &dshm_mounts + - {mountPath: /dev/shm, name: dshm} + dshm16: &dshm_16 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 16Gi} + dshm64: &dshm_64 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 64Gi} + +# namespace intentionally omitted — CLI auto-infers `default` from kube context. +image: nvcr.io/nvidian/nemo-rl:nightly +imagePullSecrets: [nvcr-secret] +serviceAccount: nemo-rl-endpoint-registry + +launch: + mode: attach + attach: + training: raycluster-single-qwen3-4b-gb300 + peerWatcher: false + entrypoint: | + pip install kubernetes -q || true + # Data files are pre-staged on the pods via kubectl cp. + export DISAGG_TRAIN_PATH=/tmp/train.jsonl + export DISAGG_VALID_PATH=/tmp/validation.jsonl + # NOTE: PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True is incompatible + # with colocated vLLM's CuMemAllocator (AssertionError at vLLM init). + export RAY_enable_infeasible_task_early_exit=true + export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM + python -u examples/nemo_gym/run_grpo_nemo_gym.py \ + --config nrl_k8s_run.yaml \ + ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ + ~policy.optimizer.kwargs.foreach \ + ~policy.optimizer.kwargs.fused + env: {} + rayUploadPaths: + - nemo_rl + - examples + - infra/examples + - tests/check_metrics.py + - tests/json_dump_tb_logs.py + - 3rdparty/Gym-workspace/Gym/nemo_gym + - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model + - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent + - 3rdparty/Gym-workspace/Gym/pyproject.toml + - 3rdparty/Gym-workspace/Gym/uv.lock + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/app.py + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/requirements.txt + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/tests + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/validation.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example_rollouts.jsonl + - 3rdparty/Megatron-LM-workspace/Megatron-LM/megatron + +clusters: + # ---------------- Training (GPU head + worker: generation + gym colocated) ---------------- + training: + name: raycluster-single-qwen3-4b-gb300 + labels: + disagg.nemo-rl/cluster: single-qwen3-4b-gb300 + disagg.nemo-rl/run: qwen3-4b-single-gb300 + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + containers: + - name: ray-head + env: + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: + name: wandb-api-key + key: WANDB_API_KEY + resources: *shared_head_resources + ports: *gpu_head_ports + volumeMounts: *dshm_mounts + volumes: *dshm_16 + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: {num-gpus: "4", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: *shared_node_selector + containers: + - name: ray-worker + env: *shared_worker_env + resources: *gpu_worker_resources + volumeMounts: *dshm_mounts + volumes: *dshm_64 diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml new file mode 100644 index 00000000000..e514e0d6a69 --- /dev/null +++ b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml @@ -0,0 +1,170 @@ +# Prod-mode infra for qwen3_4b_if_single.yaml on GB300. +# +# Same topology as qwen3_4b_if_single.gb300.infra.yaml (1 node × 4 GPUs on +# p6e-gb300r.36xlarge), but submission is prod-shaped: +# +# submit.submitter portForward — Ray Job SDK via kubectl port-forward. +# launch.runMode batch — CLI returns after Ray accepts the job. +# launch.codeSource image — no working_dir upload; code already +# on the pod's filesystem. +# launch.codePath /opt/nemo-rl — the RL repo lives here inside every +# head + worker pod, served off the +# shared FSx Lustre PVC via subPath. +# +# Because /opt/nemo-rl is the Lustre checkout at hemild/rl-k8s, edits on Lustre +# show up in the next submission without rebuilding the image. That's the whole +# point of prod mode: iterate on code without churning the container. +# +# Prereqs (onboarding §3–§5 already applied in `default`): +# - rl-workspace PVC bound (shared FSx Lustre, 2.4 TiB, RWX) +# - /mnt/rl-workspace/hemild/rl-k8s contains a git checkout of +# https://github.com/NVIDIA-NeMo/RL on branch hemil/k8s-infra-cp +# - nvcr-secret, wandb-api-key, nemo-rl-endpoint-registry all present +# +# Usage: +# # One-time per cluster: +# nrl-k8s cluster up tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml \ +# --role training +# +# # Per run — returns fast, Ray job continues in the background: +# nrl-k8s launch tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml \ +# --run-id single-if-$(date +%Y%m%d-%H%M%S) + +_shared: + nodeSelector: &shared_node_selector + nvidia.com/gpu.product: NVIDIA-GB300 + headEnv: &shared_head_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + workerEnv: &shared_worker_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + headResources: &shared_head_resources + limits: {cpu: "8", memory: "32Gi"} + requests: {cpu: "2", memory: "8Gi"} + gpuWorkerResources: &gpu_worker_resources + # p6e-gb300r.36xlarge allocatable: ~139.6 CPU / ~924 GiB / 4 GPUs. + limits: + cpu: "128" + memory: "880Gi" + nvidia.com/gpu: "4" + requests: + cpu: "128" + memory: "880Gi" + nvidia.com/gpu: "4" + gpuHeadPorts: &gpu_head_ports + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + # Volumes. The Lustre PVC is mounted at /opt/nemo-rl (subPath hemild/rl-k8s) + # for code, and at /mnt/rl-workspace (no subPath) for run outputs, caches, + # and checkpoints. dshm stays as an emptyDir for Ray's object store. + codeMounts: &code_mounts + - {mountPath: /dev/shm, name: dshm} + - {mountPath: /opt/nemo-rl, name: rl-workspace, subPath: hemild/rl-k8s} + - {mountPath: /mnt/rl-workspace, name: rl-workspace} + headVolumes: &head_volumes + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 16Gi} + - name: rl-workspace + persistentVolumeClaim: {claimName: rl-workspace} + workerVolumes: &worker_volumes + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 64Gi} + - name: rl-workspace + persistentVolumeClaim: {claimName: rl-workspace} + +# namespace auto-inferred from kube context (default). +image: nvcr.io/nvidian/nemo-rl:nightly +imagePullSecrets: [nvcr-secret] +serviceAccount: nemo-rl-endpoint-registry + +submit: + submitter: portForward + +launch: + mode: attach + runMode: batch + codeSource: image + codePath: /opt/nemo-rl + attach: + training: raycluster-single-qwen3-4b-gb300-prod + peerWatcher: false + env: {} + # Runs inside the training head pod under Ray's Job SDK. cwd inherits + # from the pod's default, so `cd /opt/nemo-rl` is explicit. The config + # path is repo-relative — load_config follows the recipe's `defaults:` + # up to grpo_qwen3_4b_instruct_k8s_base.yaml automatically. + entrypoint: | + set -eu + cd /opt/nemo-rl + # Full instruction-following dataset (~20K prompts) snapshotted from + # huggingface.co/datasets/nvidia/Nemotron-RL-instruction_following onto the + # shared Lustre workspace. + IF_DATA=/mnt/rl-workspace/hemild/datasets/instruction_following/instruction_following.jsonl + export DISAGG_TRAIN_PATH=$IF_DATA + export DISAGG_VALID_PATH=$IF_DATA + export RAY_enable_infeasible_task_early_exit=true + export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM + + python -u examples/nemo_gym/run_grpo_nemo_gym.py \ + --config tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ + ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ + ~policy.optimizer.kwargs.foreach \ + ~policy.optimizer.kwargs.fused + +clusters: + training: + name: raycluster-single-qwen3-4b-gb300-prod + labels: + disagg.nemo-rl/cluster: single-qwen3-4b-gb300-prod + disagg.nemo-rl/run: qwen3-4b-single-gb300-prod + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + containers: + - name: ray-head + env: + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: + name: wandb-api-key + key: WANDB_API_KEY + resources: *shared_head_resources + ports: *gpu_head_ports + volumeMounts: *code_mounts + volumes: *head_volumes + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: {num-gpus: "4", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: *shared_node_selector + containers: + - name: ray-worker + env: *shared_worker_env + resources: *gpu_worker_resources + volumeMounts: *code_mounts + volumes: *worker_volumes diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_single.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_single.yaml index fea7d828533..9ec4dd473af 100644 --- a/tools/nrl_k8s/examples/qwen3_4b_if_single.yaml +++ b/tools/nrl_k8s/examples/qwen3_4b_if_single.yaml @@ -10,12 +10,12 @@ defaults: ../../../examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml cluster: - gpus_per_node: 8 + gpus_per_node: 4 num_nodes: 1 grpo: num_prompts_per_step: 32 num_generations_per_prompt: 16 - max_num_steps: 200 + max_num_steps: 500 val_period: 50 max_rollout_turns: 1 async_grpo: @@ -35,7 +35,7 @@ policy: colocated: enabled: true resources: - gpus_per_node: 8 + gpus_per_node: 4 num_nodes: 1 vllm_cfg: gpu_memory_utilization: 0.45 # leave 55% for training state From 5c4d1c31769f90bea62fa9935753186ff0851086 Mon Sep 17 00:00:00 2001 From: Hemil Desai Date: Wed, 22 Apr 2026 15:15:41 -0700 Subject: [PATCH 32/84] feat(nrl-k8s): rayjob + go CLI commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new ways to drive a recipe: * `nrl-k8s rayjob ` — submits the declared training cluster as a KubeRay RayJob (submissionMode HTTPMode, shutdownAfterJobFinishes, ttlSecondsAfterFinished). The rayClusterSpec reuses build_raycluster_manifest so image/imagePullSecrets/SA/labels are patched the same way as standalone clusters. Flags: --name, --shutdown/--no-shutdown, --ttl, --wait/--no-wait, --timeout, --dry-run. Exits 0 on Complete and 1 on Failed. * `nrl-k8s go ` — idempotent run: for each declared role, reuse the live RayCluster when its stripped spec matches the rendered manifest, apply when absent, warn + reuse on drift (pass --recreate to delete and re-apply). Then submit daemons and training as `launch` would. --skip-daemons lets you re-submit training on an already-healthy disagg setup without touching gym/generation. Wires: * src/nrl_k8s/rayjob.py — build_rayjob_manifest. * src/nrl_k8s/k8s.py — apply_rayjob, get_rayjob, delete_rayjob, wait_for_rayjob_terminal (polls jobDeploymentStatus until Complete/Failed, surfaces transitions through on_update callback). * src/nrl_k8s/orchestrate.py — ensure_cluster (idempotent apply + drift diff via _strip_server_fields) and go (run-equivalent using it). * src/nrl_k8s/cli.py — @main.command rayjob, @main.command go. Tests: 161 -> 173 passed. New coverage in test_rayjob.py, TestRayJob in test_cli.py, TestEnsureCluster + TestGo in test_orchestrate.py and TestGoCommand in test_cli.py. Signed-off-by: Hemil Desai Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Hemil Desai Signed-off-by: Terry Kong --- tools/nrl_k8s/src/nrl_k8s/cli.py | 232 +++++++++++++++++++ tools/nrl_k8s/src/nrl_k8s/k8s.py | 123 ++++++++++ tools/nrl_k8s/src/nrl_k8s/orchestrate.py | 112 +++++++++ tools/nrl_k8s/src/nrl_k8s/rayjob.py | 100 ++++++++ tools/nrl_k8s/tests/unit/test_cli.py | 184 +++++++++++++++ tools/nrl_k8s/tests/unit/test_orchestrate.py | 148 ++++++++++++ tools/nrl_k8s/tests/unit/test_rayjob.py | 152 ++++++++++++ 7 files changed, 1051 insertions(+) create mode 100644 tools/nrl_k8s/src/nrl_k8s/rayjob.py create mode 100644 tools/nrl_k8s/tests/unit/test_rayjob.py diff --git a/tools/nrl_k8s/src/nrl_k8s/cli.py b/tools/nrl_k8s/src/nrl_k8s/cli.py index 15307ffb763..d5b9b9c66fb 100644 --- a/tools/nrl_k8s/src/nrl_k8s/cli.py +++ b/tools/nrl_k8s/src/nrl_k8s/cli.py @@ -508,6 +508,238 @@ def run( _follow_handle(result.handle) +@main.command() +@click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) +@_INFRA_OPTION +@click.option( + "--repo-root", + type=click.Path(exists=True, file_okay=False, path_type=Path), + default=Path.cwd(), + show_default="cwd", + help="NeMo-RL repo root used to source files for the working_dir upload.", +) +@click.option( + "--replace", + is_flag=True, + help="Stop any running daemon/training job before submitting new ones.", +) +@click.option( + "--recreate", + is_flag=True, + help="Delete + re-apply any RayCluster whose live spec has drifted from " + "the rendered manifest. Default is to warn and reuse as-is.", +) +@click.option( + "--skip-daemons", + is_flag=True, + help="Bring up every declared cluster but only submit training — skip " + "daemons on gym/generation roles.", +) +@_mode_options +def go( + recipe: Path, + overrides: tuple[str, ...], + infra_path: Path | None, + repo_root: Path, + replace: bool, + recreate: bool, + skip_daemons: bool, + cli_mode: str | None, + cli_submitter: str | None, + cli_code_source: str | None, + cli_code_path: str | None, + cli_run_id: str | None, + cli_wait: bool | None, +) -> None: + """Idempotent ``run``: reuse matching clusters, then launch training. + + For each role declared in the recipe, reuse the live RayCluster when + its spec matches the rendered manifest, apply when it is absent, and + warn + reuse when it has drifted (pass ``--recreate`` to delete + + re-apply). Then submit daemons and the training entrypoint, same as + :command:`launch`. + + Use ``--skip-daemons`` on a disaggregated recipe when the gym / + generation daemons are already healthy and you only want to re-submit + training. + """ + from . import orchestrate + from . import submit as submit_mod + + loaded = _load_or_exit(recipe, overrides, infra_path) + if not submit_mod.is_in_cluster(): + _preflight_or_exit(loaded.infra.namespace) + + mode, submitter, code_src, no_wait = _resolve_mode_defaults( + cli_mode=cli_mode, + infra_mode=loaded.infra.launch.runMode, + cli_submitter=cli_submitter, + cli_code_source=cli_code_source, + cli_wait=cli_wait, + ) + _apply_mode_overrides(loaded, submitter=submitter, code_source=code_src, code_path=cli_code_path) + click.echo( + f"[go] mode={mode.value} submitter={submitter.value} " + f"code_source={code_src.value} no_wait={no_wait} " + f"recreate={recreate} skip_daemons={skip_daemons}", + err=True, + ) + + try: + result = orchestrate.go( + loaded, + log=click.echo, + repo_root=repo_root.resolve(), + replace=replace, + run_id=cli_run_id, + skip_daemons=skip_daemons, + recreate=recreate, + ) + except Exception as exc: # noqa: BLE001 + _explain_and_exit(exc, context="go failed") + + _emit_handle(result.handle) + if not no_wait: + _follow_handle(result.handle) + + +@main.command("rayjob") +@click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) +@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) +@_INFRA_OPTION +@click.option( + "--name", + "name_opt", + type=str, + default=None, + help="RayJob metadata name. Defaults to the training cluster name.", +) +@click.option( + "--shutdown/--no-shutdown", + "shutdown_after", + default=True, + show_default=True, + help="Delete the ephemeral RayCluster once the Ray Job reaches a " + "terminal state (KubeRay's shutdownAfterJobFinishes).", +) +@click.option( + "--ttl", + "ttl_seconds", + type=int, + default=3600, + show_default=True, + help="Seconds to keep the RayJob object after the run finishes " + "(ttlSecondsAfterFinished). Useful for post-mortem log access.", +) +@click.option( + "--wait/--no-wait", + default=True, + help="Poll the RayJob until jobDeploymentStatus is Complete or Failed. " + "Pass --no-wait to return as soon as the RayJob is applied.", +) +@click.option( + "--timeout", + "timeout_s", + type=int, + default=86400, + show_default=True, + help="Seconds to wait for the RayJob to reach a terminal state.", +) +@click.option( + "--dry-run", + is_flag=True, + help="Render the RayJob manifest and print it; do not apply.", +) +def rayjob( + recipe: Path, + overrides: tuple[str, ...], + infra_path: Path | None, + name_opt: str | None, + shutdown_after: bool, + ttl_seconds: int, + wait: bool, + timeout_s: int, + dry_run: bool, +) -> None: + """Submit the recipe as an ephemeral RayJob. + + KubeRay creates the RayCluster declared under ``clusters.training``, + submits ``infra.launch.entrypoint`` over the dashboard HTTP API, polls + until the driver is terminal, then tears the cluster down. Convenient + for one-shot runs where you don't want a long-lived RayCluster left + over after the job finishes. + + Contrast with ``nrl-k8s run`` / ``launch`` which attach a job to a + long-lived cluster and leave the cluster up for future submissions. + """ + from . import k8s + from . import submit as submit_mod + from .rayjob import build_rayjob_manifest + + loaded = _load_or_exit(recipe, overrides, infra_path) + cluster = _pick_cluster_or_exit(loaded, "training") + if not loaded.infra.launch.entrypoint: + _cli_error( + "infra.launch.entrypoint is empty", + hint="rayjob requires infra.launch.entrypoint; see docs/recipes.md", + ) + + manifest = build_rayjob_manifest( + cluster, + loaded.infra, + entrypoint=loaded.infra.launch.entrypoint, + name=name_opt, + shutdown_after_finishes=shutdown_after, + ttl_seconds_after_finished=ttl_seconds, + ) + job_name = manifest["metadata"]["name"] + namespace = loaded.infra.namespace + + if dry_run: + click.echo(yaml.safe_dump(manifest, sort_keys=False).rstrip()) + return + + if not submit_mod.is_in_cluster(): + _preflight_or_exit(namespace) + + click.echo(f"[rayjob] applying RayJob {job_name} in {namespace}") + try: + k8s.apply_rayjob(manifest, namespace) + except ApiException as exc: + _explain_and_exit(exc, context=f"rayjob {job_name} apply failed") + except Exception as exc: # noqa: BLE001 + _explain_and_exit(exc, context=f"rayjob {job_name} apply failed") + + click.echo( + f"follow: kubectl get rayjob {job_name} -n {namespace} -w\n" + f"logs: nrl-k8s job logs {recipe} --role training -f", + ) + if not wait: + return + + click.echo(f"[rayjob] waiting for {job_name} to reach a terminal state ...") + + def _on_update(deployment: str | None, job: str | None) -> None: + click.echo(f"[rayjob] {job_name} deployment={deployment} job={job}") + + try: + final = k8s.wait_for_rayjob_terminal( + job_name, namespace, timeout_s=timeout_s, on_update=_on_update + ) + except Exception as exc: # noqa: BLE001 + _explain_and_exit(exc, context=f"rayjob {job_name} wait failed") + + status = final.get("status") or {} + dep = status.get("jobDeploymentStatus") + job_status = status.get("jobStatus") + message = (status.get("message") or "").strip() + click.echo(f"[rayjob] {job_name} finished: deployment={dep} job={job_status}") + if message: + click.echo(f"[rayjob] message: {message}") + sys.exit(0 if dep == "Complete" else 1) + + @main.command() @click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) @click.argument("overrides", nargs=-1, type=click.UNPROCESSED) diff --git a/tools/nrl_k8s/src/nrl_k8s/k8s.py b/tools/nrl_k8s/src/nrl_k8s/k8s.py index af4bf71581d..b7166827843 100644 --- a/tools/nrl_k8s/src/nrl_k8s/k8s.py +++ b/tools/nrl_k8s/src/nrl_k8s/k8s.py @@ -22,6 +22,12 @@ RAY_GROUP = "ray.io" RAY_VERSION = "v1" RAYCLUSTER_PLURAL = "rayclusters" +RAYJOB_PLURAL = "rayjobs" + +# RayJob jobDeploymentStatus terminal states. `Complete` covers a job that +# succeeded end-to-end; `Failed` covers driver exit != 0; the rest are +# actively-running / suspended states where we keep polling. +_RAYJOB_TERMINAL_DEPLOYMENT = ("Complete", "Failed") # ============================================================================= @@ -168,6 +174,119 @@ def wait_for_raycluster_gone( raise TimeoutError(f"RayCluster {name} not deleted after {timeout_s}s") +# ============================================================================= +# RayJob lifecycle +# ============================================================================= + + +def apply_rayjob(manifest: dict[str, Any], namespace: str) -> dict[str, Any]: + """Create-or-replace a RayJob. Returns the server-side object.""" + name = manifest["metadata"]["name"] + api = custom_objects_api() + try: + return with_retries( + lambda: api.create_namespaced_custom_object( + group=RAY_GROUP, + version=RAY_VERSION, + namespace=namespace, + plural=RAYJOB_PLURAL, + body=manifest, + ) + ) + except ApiException as exc: + if exc.status == 409: + return with_retries( + lambda: api.patch_namespaced_custom_object( + group=RAY_GROUP, + version=RAY_VERSION, + namespace=namespace, + plural=RAYJOB_PLURAL, + name=name, + body=manifest, + ) + ) + exc.nrl_k8s_manifest = redact(manifest) # type: ignore[attr-defined] + raise + + +def delete_rayjob( + name: str, namespace: str, *, ignore_missing: bool = True +) -> None: + api = custom_objects_api() + try: + with_retries( + lambda: api.delete_namespaced_custom_object( + group=RAY_GROUP, + version=RAY_VERSION, + namespace=namespace, + plural=RAYJOB_PLURAL, + name=name, + ) + ) + except ApiException as exc: + if exc.status == 404 and ignore_missing: + return + raise + + +def get_rayjob(name: str, namespace: str) -> dict[str, Any] | None: + api = custom_objects_api() + try: + return with_retries( + lambda: api.get_namespaced_custom_object( + group=RAY_GROUP, + version=RAY_VERSION, + namespace=namespace, + plural=RAYJOB_PLURAL, + name=name, + ) + ) + except ApiException as exc: + if exc.status == 404: + return None + raise + + +def wait_for_rayjob_terminal( + name: str, + namespace: str, + *, + timeout_s: int = 86400, + poll_s: int = 10, + on_update: callable | None = None, +) -> dict[str, Any]: + """Block until a RayJob's ``jobDeploymentStatus`` is Complete or Failed. + + Returns the final server-side object so callers can read the + ``.status.jobStatus`` / ``.status.message`` fields. ``on_update`` is + invoked with ``(jobDeploymentStatus, jobStatus)`` on every transition — + useful for printing progress without this module owning the logger. + """ + deadline = time.monotonic() + timeout_s + last: tuple[str | None, str | None] = (None, None) + while time.monotonic() < deadline: + obj = get_rayjob(name, namespace) + if obj is None: + raise RuntimeError( + f"RayJob {name} disappeared from {namespace} before reaching terminal state" + ) + status = obj.get("status") or {} + dep = status.get("jobDeploymentStatus") + job = status.get("jobStatus") + if (dep, job) != last: + if on_update is not None: + on_update(dep, job) + last = (dep, job) + if dep in _RAYJOB_TERMINAL_DEPLOYMENT: + return obj + time.sleep(poll_s) + raise TimeoutError( + f"RayJob {name} in {namespace} did not reach a terminal state " + f"(last seen jobDeploymentStatus={last[0]!r}, jobStatus={last[1]!r}) " + f"after {timeout_s}s" + ) + + def delete_configmap(name: str, namespace: str, *, ignore_missing: bool = True) -> bool: """Delete a ConfigMap. Returns True if deleted, False if it didn't exist.""" load_kubeconfig() @@ -210,13 +329,17 @@ def get_head_pod(cluster_name: str, namespace: str) -> Any: __all__ = [ "apply_raycluster", + "apply_rayjob", "custom_objects_api", "delete_configmap", "delete_raycluster", + "delete_rayjob", "get_head_pod", "get_raycluster", + "get_rayjob", "list_rayclusters", "load_kubeconfig", "wait_for_raycluster_gone", "wait_for_raycluster_ready", + "wait_for_rayjob_terminal", ] diff --git a/tools/nrl_k8s/src/nrl_k8s/orchestrate.py b/tools/nrl_k8s/src/nrl_k8s/orchestrate.py index c94269dacbb..bad3c0f9c52 100644 --- a/tools/nrl_k8s/src/nrl_k8s/orchestrate.py +++ b/tools/nrl_k8s/src/nrl_k8s/orchestrate.py @@ -89,6 +89,87 @@ def bring_up_cluster( return name +def ensure_cluster( + role: Role, + loaded: LoadedConfig, + *, + log: callable, + recreate: bool = False, + wait_ready: bool = True, + ready_timeout_s: int = 900, +) -> str: + """Idempotent cluster up: reuse when live matches rendered, warn on drift. + + Unlike :func:`bring_up_cluster`, we never silently patch a live cluster. + If the RayCluster already exists and its spec matches the rendered one + we just wait for readiness. If it exists but drifted we log a warning + and reuse anyway — pass ``recreate=True`` to delete + re-apply instead. + """ + cluster = _require_cluster(loaded.infra, role) + manifest = build_raycluster_manifest(cluster, loaded.infra) + name = cluster.name + namespace = loaded.infra.namespace + + live = k8s.get_raycluster(name, namespace) + if live is None: + log(f"[{role}] applying RayCluster {name} in namespace {namespace}") + k8s.apply_raycluster(manifest, namespace) + elif _spec_drifted(live.get("spec") or {}, manifest["spec"]): + if recreate: + log( + f"[{role}] --recreate: RayCluster {name} has drifted from the " + f"rendered manifest; deleting and re-applying" + ) + k8s.delete_raycluster(name, namespace) + k8s.wait_for_raycluster_gone(name, namespace) + k8s.apply_raycluster(manifest, namespace) + else: + log( + f"[{role}] warning: live RayCluster {name} has drifted from the " + f"rendered manifest; reusing as-is (pass --recreate to replace)" + ) + else: + log(f"[{role}] RayCluster {name} already exists and matches — reusing") + + if wait_ready: + log(f"[{role}] waiting for RayCluster {name} to reach state=ready ...") + k8s.wait_for_raycluster_ready(name, namespace, timeout_s=ready_timeout_s) + log(f"[{role}] RayCluster {name} is ready.") + + return name + + +# Server-managed fields that never appear in a rendered manifest and should +# be ignored when diffing for drift. +_DRIFT_IGNORE_TOP = ("status",) +_DRIFT_IGNORE_METADATA = ( + "creationTimestamp", + "generation", + "managedFields", + "resourceVersion", + "selfLink", + "uid", +) + + +def _spec_drifted(live_spec: dict, rendered_spec: dict) -> bool: + """Return True if the live RayCluster ``.spec`` diverges from rendered.""" + return _strip_server_fields(live_spec) != _strip_server_fields(rendered_spec) + + +def _strip_server_fields(obj): + """Recursively drop keys the API server injects so comparisons are stable.""" + if isinstance(obj, dict): + return { + k: _strip_server_fields(v) + for k, v in obj.items() + if k not in _DRIFT_IGNORE_TOP and k not in _DRIFT_IGNORE_METADATA + } + if isinstance(obj, list): + return [_strip_server_fields(v) for v in obj] + return obj + + def submit_daemon( role: Role, loaded: LoadedConfig, @@ -304,6 +385,35 @@ def run( ) +def go( + loaded: LoadedConfig, + *, + log: callable, + repo_root: Path, + replace: bool = False, + run_id: str | None = None, + skip_daemons: bool = False, + recreate: bool = False, +) -> RunResult: + """Idempotent ``run``: reuse matching clusters, warn on drift, then launch.""" + if replace: + _reset_endpoint_registry(loaded, log=log) + + for role in ALL_ROLES: + if _get_cluster(loaded.infra, role) is None: + log(f"[{role}] not defined in recipe — skipping") + continue + name = ensure_cluster(role, loaded, log=log, recreate=recreate) + if skip_daemons and role != "training": + log(f"[{role}] --skip-daemons: not submitting daemon") + continue + submit_daemon(role, loaded, name, log=log, repo_root=repo_root, replace=replace) + + return submit_training( + loaded, log=log, repo_root=repo_root, replace=replace, run_id=run_id + ) + + _JOB_ID_RE = re.compile(r"--job-id[= ]+(\S+)") @@ -404,6 +514,8 @@ def _wait_for_http(url: str, timeout_s: int, log: callable, role: Role) -> None: "RunResult", "bring_up_cluster", "default_run_id", + "ensure_cluster", + "go", "run", "submit_daemon", "submit_training", diff --git a/tools/nrl_k8s/src/nrl_k8s/rayjob.py b/tools/nrl_k8s/src/nrl_k8s/rayjob.py new file mode 100644 index 00000000000..27ae4de0976 --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/rayjob.py @@ -0,0 +1,100 @@ +"""Build and apply KubeRay ``RayJob`` objects. + +A RayJob is a RayCluster + Ray Job rolled into one K8s resource: KubeRay +creates the cluster, waits for it to become ready, submits ``spec.entrypoint`` +over the dashboard HTTP API, polls until terminal, then (optionally) tears +the whole cluster down. It is the convenient shape for one-shot runs where +you do *not* want a long-lived RayCluster left behind. + +The inline RayCluster body comes from the recipe — this module reuses +:func:`nrl_k8s.manifest.build_raycluster_manifest` to get the patched cluster +manifest, then lifts its ``.spec`` under ``rayClusterSpec`` and wraps a +RayJob envelope around it. +""" + +from __future__ import annotations + +from typing import Any + +from .manifest import build_raycluster_manifest +from .schema import ClusterSpec, InfraConfig + +# KubeRay defaults recipes have standardised on. +DEFAULT_SUBMISSION_MODE = "HTTPMode" +DEFAULT_TTL_SECONDS = 3600 + + +def build_rayjob_manifest( + cluster: ClusterSpec, + infra: InfraConfig, + *, + entrypoint: str, + name: str | None = None, + shutdown_after_finishes: bool = True, + ttl_seconds_after_finished: int = DEFAULT_TTL_SECONDS, + submission_mode: str = DEFAULT_SUBMISSION_MODE, + extra_labels: dict[str, str] | None = None, +) -> dict[str, Any]: + """Wrap a RayCluster inline body in a RayJob envelope. + + Args: + cluster: the training ClusterSpec used for ``rayClusterSpec``. + infra: top-level infra — supplies namespace, image, pullSecrets, SA. + These are applied inside the rayClusterSpec identically to how + the standalone RayCluster path patches them. + entrypoint: shell command KubeRay submits via HTTP to the dashboard. + Typically ``infra.launch.entrypoint``. + name: RayJob metadata name. Defaults to the cluster's name — + convenient because ``nrl-k8s job list`` on an ephemeral cluster + can still resolve by that name. + shutdown_after_finishes: KubeRay deletes the RayCluster once the + Ray Job reaches a terminal state. Leave True for one-shot runs. + ttl_seconds_after_finished: seconds to keep the RayJob object + around after the job reaches Complete/Failed. After the TTL + the garbage collector removes the RayJob (and the cluster, + when ``shutdown_after_finishes=True``). + submission_mode: KubeRay submission path. ``HTTPMode`` posts the + entrypoint to the dashboard (same code path as Ray Job SDK). + ``K8sJobMode`` creates a separate K8s Job, which breaks KAI + gang scheduling — leave this at the default unless you know + you need the other shape. + extra_labels: merged on top of infra + cluster labels on the RayJob. + Used by callers to tag runs (e.g. ``disagg.nemo-rl/run-id``). + """ + # Reuse the RayCluster builder so image / imagePullSecrets / SA / labels + # are patched the same way as the standalone cluster path. Then lift + # the .spec out; RayJob nests it under rayClusterSpec. + cluster_manifest = build_raycluster_manifest(cluster, infra) + ray_cluster_spec = cluster_manifest["spec"] + + job_name = name or cluster.name + metadata: dict[str, Any] = { + "name": job_name, + "namespace": infra.namespace, + } + merged_labels = {**infra.labels, **cluster.labels, **(extra_labels or {})} + if merged_labels: + metadata["labels"] = merged_labels + merged_annotations = {**infra.annotations, **cluster.annotations} + if merged_annotations: + metadata["annotations"] = merged_annotations + + return { + "apiVersion": "ray.io/v1", + "kind": "RayJob", + "metadata": metadata, + "spec": { + "entrypoint": entrypoint, + "submissionMode": submission_mode, + "shutdownAfterJobFinishes": shutdown_after_finishes, + "ttlSecondsAfterFinished": ttl_seconds_after_finished, + "rayClusterSpec": ray_cluster_spec, + }, + } + + +__all__ = [ + "DEFAULT_SUBMISSION_MODE", + "DEFAULT_TTL_SECONDS", + "build_rayjob_manifest", +] diff --git a/tools/nrl_k8s/tests/unit/test_cli.py b/tools/nrl_k8s/tests/unit/test_cli.py index a8e18332fe3..0cfdb32c3dd 100644 --- a/tools/nrl_k8s/tests/unit/test_cli.py +++ b/tools/nrl_k8s/tests/unit/test_cli.py @@ -255,6 +255,190 @@ def test_no_fix_skips_reinstall(self, monkeypatch): assert fix_called == [] +# ============================================================================= +# rayjob — ephemeral RayJob submission +# ============================================================================= + + +class TestRayJob: + @staticmethod + def _recipe_with_training(tmp_path: Path, entrypoint: str | None) -> Path: + spec = { + "headGroupSpec": { + "template": {"spec": {"containers": [{"name": "h", "image": "old"}]}} + } + } + infra = { + "namespace": "ns", + "image": "img:new", + "clusters": {"training": {"name": "rc-train", "spec": spec}}, + } + if entrypoint is not None: + infra["launch"] = {"entrypoint": entrypoint} + return _write_recipe(tmp_path, {"infra": infra}) + + def test_dry_run_prints_manifest_without_applying(self, tmp_path, monkeypatch): + recipe = self._recipe_with_training(tmp_path, "python run.py") + + applied: list[dict] = [] + monkeypatch.setattr( + "nrl_k8s.k8s.apply_rayjob", + lambda manifest, ns: applied.append((manifest, ns)), + ) + + runner = CliRunner() + result = runner.invoke( + cli.main, + ["rayjob", str(recipe), "--dry-run"], + ) + assert result.exit_code == 0, result.output + assert applied == [] + assert "kind: RayJob" in result.output + assert "entrypoint: python run.py" in result.output + + def test_apply_then_wait_success(self, tmp_path, monkeypatch): + recipe = self._recipe_with_training(tmp_path, "echo") + + applied: list[tuple[dict, str]] = [] + + def _fake_apply(manifest, ns): + applied.append((manifest, ns)) + return manifest + + def _fake_wait(name, namespace, *, timeout_s, on_update=None): + return { + "metadata": {"name": name}, + "status": { + "jobDeploymentStatus": "Complete", + "jobStatus": "SUCCEEDED", + }, + } + + monkeypatch.setattr("nrl_k8s.k8s.apply_rayjob", _fake_apply) + monkeypatch.setattr("nrl_k8s.k8s.wait_for_rayjob_terminal", _fake_wait) + monkeypatch.setattr("nrl_k8s.submit.is_in_cluster", lambda: True) + + runner = CliRunner() + result = runner.invoke(cli.main, ["rayjob", str(recipe)]) + assert result.exit_code == 0, result.output + assert len(applied) == 1 + manifest, ns = applied[0] + assert ns == "ns" + assert manifest["kind"] == "RayJob" + assert manifest["metadata"]["name"] == "rc-train" + assert manifest["spec"]["entrypoint"] == "echo" + assert manifest["spec"]["shutdownAfterJobFinishes"] is True + + def test_failed_job_exits_non_zero(self, tmp_path, monkeypatch): + recipe = self._recipe_with_training(tmp_path, "echo") + monkeypatch.setattr( + "nrl_k8s.k8s.apply_rayjob", lambda m, ns: m + ) + monkeypatch.setattr( + "nrl_k8s.k8s.wait_for_rayjob_terminal", + lambda *a, **kw: { + "status": {"jobDeploymentStatus": "Failed", "jobStatus": "FAILED"} + }, + ) + monkeypatch.setattr("nrl_k8s.submit.is_in_cluster", lambda: True) + + runner = CliRunner() + result = runner.invoke(cli.main, ["rayjob", str(recipe)]) + assert result.exit_code == 1 + + def test_no_wait_skips_poll(self, tmp_path, monkeypatch): + recipe = self._recipe_with_training(tmp_path, "echo") + waited: list[int] = [] + + monkeypatch.setattr("nrl_k8s.k8s.apply_rayjob", lambda m, ns: m) + monkeypatch.setattr( + "nrl_k8s.k8s.wait_for_rayjob_terminal", + lambda *a, **kw: waited.append(1) or {}, + ) + monkeypatch.setattr("nrl_k8s.submit.is_in_cluster", lambda: True) + + runner = CliRunner() + result = runner.invoke(cli.main, ["rayjob", str(recipe), "--no-wait"]) + assert result.exit_code == 0, result.output + assert waited == [] + + def test_errors_when_entrypoint_missing(self, tmp_path): + recipe = self._recipe_with_training(tmp_path, entrypoint=None) + runner = CliRunner() + result = runner.invoke(cli.main, ["rayjob", str(recipe), "--dry-run"]) + assert result.exit_code == 1 + assert "entrypoint" in result.output + + +class TestGoCommand: + """`nrl-k8s go` delegates to orchestrate.go with the CLI's resolved flags.""" + + def test_go_invokes_orchestrate_with_flags(self, tmp_path, monkeypatch) -> None: + spec = { + "headGroupSpec": { + "template": {"spec": {"containers": [{"name": "h", "image": "old"}]}} + } + } + recipe = _write_recipe( + tmp_path, + { + "infra": { + "namespace": "ns", + "image": "img:new", + "launch": {"entrypoint": "python run.py"}, + "clusters": {"training": {"name": "rc-train", "spec": spec}}, + } + }, + ) + + captured: dict = {} + + class _FakeHandle: + run_id = "training-1" + kind = "port-forward" + cluster_name = "rc-train" + namespace = "ns" + pod = None + tmp_dir = None + + class _FakeResult: + handle = _FakeHandle() + + def _fake_go(loaded, *, log, repo_root, replace, run_id, skip_daemons, recreate): + captured["skip_daemons"] = skip_daemons + captured["recreate"] = recreate + captured["replace"] = replace + captured["run_id"] = run_id + return _FakeResult() + + monkeypatch.setattr("nrl_k8s.orchestrate.go", _fake_go) + monkeypatch.setattr("nrl_k8s.submit.is_in_cluster", lambda: True) + + runner = CliRunner() + result = runner.invoke( + cli.main, + [ + "go", + str(recipe), + "--mode", "batch", + "--code-source", "image", + "--code-path", "/opt/nemo-rl", + "--run-id", "run-x", + "--skip-daemons", + "--recreate", + "--no-wait", + ], + ) + assert result.exit_code == 0, result.output + assert captured == { + "skip_daemons": True, + "recreate": True, + "replace": False, + "run_id": "run-x", + } + assert "run id: training-1" in result.output + + class TestClusterDown: def test_errors_without_role_or_name(self, tmp_path, monkeypatch) -> None: recipe = _write_recipe( diff --git a/tools/nrl_k8s/tests/unit/test_orchestrate.py b/tools/nrl_k8s/tests/unit/test_orchestrate.py index c8d5f87bc8b..26cb809b844 100644 --- a/tools/nrl_k8s/tests/unit/test_orchestrate.py +++ b/tools/nrl_k8s/tests/unit/test_orchestrate.py @@ -256,6 +256,154 @@ def test_raises_when_entrypoint_unset(self, monkeypatch, log) -> None: orchestrate.submit_training(loaded, log=log_fn, repo_root=Path("/tmp")) +# ============================================================================= +# ensure_cluster — idempotent path used by `go` +# ============================================================================= + + +class TestEnsureCluster: + def test_applies_when_absent(self, monkeypatch, log) -> None: + log_fn, lines = log + get = MagicMock(return_value=None) + apply = MagicMock() + wait = MagicMock() + monkeypatch.setattr(orchestrate.k8s, "get_raycluster", get) + monkeypatch.setattr(orchestrate.k8s, "apply_raycluster", apply) + monkeypatch.setattr(orchestrate.k8s, "wait_for_raycluster_ready", wait) + + name = orchestrate.ensure_cluster("training", _loaded(), log=log_fn) + + assert name == "rc-train" + apply.assert_called_once() + wait.assert_called_once() + + def test_reuses_when_live_matches(self, monkeypatch, log) -> None: + log_fn, lines = log + loaded = _loaded() + rendered = orchestrate.build_raycluster_manifest( + loaded.infra.clusters.training, loaded.infra + ) + live = { + "metadata": { + "name": "rc-train", + "resourceVersion": "9", + "uid": "abc", + }, + "spec": rendered["spec"], + "status": {"state": "ready"}, + } + monkeypatch.setattr( + orchestrate.k8s, "get_raycluster", MagicMock(return_value=live) + ) + apply = MagicMock() + wait = MagicMock() + monkeypatch.setattr(orchestrate.k8s, "apply_raycluster", apply) + monkeypatch.setattr(orchestrate.k8s, "wait_for_raycluster_ready", wait) + + orchestrate.ensure_cluster("training", loaded, log=log_fn) + + apply.assert_not_called() + wait.assert_called_once() + assert any("already exists and matches" in ln for ln in lines) + + def test_warns_on_drift_and_reuses(self, monkeypatch, log) -> None: + log_fn, lines = log + loaded = _loaded() + # Start from the rendered spec, mutate one field to simulate drift. + rendered = orchestrate.build_raycluster_manifest( + loaded.infra.clusters.training, loaded.infra + ) + drifted = {"metadata": {"name": "rc-train"}, "spec": dict(rendered["spec"])} + drifted["spec"]["rayVersion"] = "drifted" + monkeypatch.setattr( + orchestrate.k8s, "get_raycluster", MagicMock(return_value=drifted) + ) + apply = MagicMock() + delete = MagicMock() + monkeypatch.setattr(orchestrate.k8s, "apply_raycluster", apply) + monkeypatch.setattr(orchestrate.k8s, "delete_raycluster", delete) + monkeypatch.setattr(orchestrate.k8s, "wait_for_raycluster_ready", MagicMock()) + + orchestrate.ensure_cluster("training", loaded, log=log_fn) + + apply.assert_not_called() + delete.assert_not_called() + assert any("drifted" in ln and "reusing" in ln for ln in lines) + + def test_recreate_deletes_and_reapplies_on_drift(self, monkeypatch, log) -> None: + log_fn, lines = log + loaded = _loaded() + rendered = orchestrate.build_raycluster_manifest( + loaded.infra.clusters.training, loaded.infra + ) + drifted = {"metadata": {"name": "rc-train"}, "spec": dict(rendered["spec"])} + drifted["spec"]["rayVersion"] = "drifted" + monkeypatch.setattr( + orchestrate.k8s, "get_raycluster", MagicMock(return_value=drifted) + ) + apply = MagicMock() + delete = MagicMock() + gone = MagicMock() + monkeypatch.setattr(orchestrate.k8s, "apply_raycluster", apply) + monkeypatch.setattr(orchestrate.k8s, "delete_raycluster", delete) + monkeypatch.setattr(orchestrate.k8s, "wait_for_raycluster_gone", gone) + monkeypatch.setattr(orchestrate.k8s, "wait_for_raycluster_ready", MagicMock()) + + orchestrate.ensure_cluster( + "training", loaded, log=log_fn, recreate=True + ) + + delete.assert_called_once_with("rc-train", "ns-a") + gone.assert_called_once() + apply.assert_called_once() + + +# ============================================================================= +# go — idempotent run +# ============================================================================= + + +class TestGo: + def test_skip_daemons_bypasses_daemon_submit(self, monkeypatch, log) -> None: + log_fn, lines = log + loaded = _loaded(gym_entrypoint="python gym.py --job-id run-q") + + ensure = MagicMock(side_effect=lambda role, *a, **kw: f"rc-{role}") + daemon = MagicMock() + train = MagicMock(return_value="TRAIN_RESULT") + monkeypatch.setattr(orchestrate, "ensure_cluster", ensure) + monkeypatch.setattr(orchestrate, "submit_daemon", daemon) + monkeypatch.setattr(orchestrate, "submit_training", train) + + out = orchestrate.go( + loaded, log=log_fn, repo_root=Path("/tmp"), skip_daemons=True + ) + + assert out == "TRAIN_RESULT" + # ensure_cluster called once per declared role. + roles_ensured = {c.args[0] for c in ensure.call_args_list} + assert roles_ensured == {"gym", "training"} + # Only training gets a submit_daemon call; gym is skipped. + roles_daemoned = [c.args[0] for c in daemon.call_args_list] + assert roles_daemoned == ["training"] + train.assert_called_once() + + def test_recreate_flag_propagates(self, monkeypatch, log) -> None: + log_fn, _ = log + loaded = _loaded() + + ensure = MagicMock(return_value="rc-train") + monkeypatch.setattr(orchestrate, "ensure_cluster", ensure) + monkeypatch.setattr(orchestrate, "submit_daemon", MagicMock()) + monkeypatch.setattr(orchestrate, "submit_training", MagicMock()) + + orchestrate.go( + loaded, log=log_fn, repo_root=Path("/tmp"), recreate=True + ) + + assert ensure.call_args.kwargs["recreate"] is True + + # ============================================================================= # _infer_disagg_job_id — regex over gym entrypoints # ============================================================================= diff --git a/tools/nrl_k8s/tests/unit/test_rayjob.py b/tools/nrl_k8s/tests/unit/test_rayjob.py new file mode 100644 index 00000000000..f0fc1744d9d --- /dev/null +++ b/tools/nrl_k8s/tests/unit/test_rayjob.py @@ -0,0 +1,152 @@ +"""Tests for :mod:`nrl_k8s.rayjob` — RayJob manifest builder.""" + +from __future__ import annotations + +from nrl_k8s.rayjob import DEFAULT_SUBMISSION_MODE, build_rayjob_manifest +from nrl_k8s.schema import ClusterSpec, InfraConfig + + +def _base_spec() -> dict: + return { + "headGroupSpec": { + "template": { + "spec": { + "containers": [{"name": "ray-head", "image": "registry/img:old"}], + } + } + }, + "workerGroupSpecs": [ + { + "groupName": "gpu-workers", + "template": { + "spec": { + "containers": [ + {"name": "ray-worker", "image": "registry/img:old"} + ], + } + }, + } + ], + } + + +def _make_infra(**overrides) -> InfraConfig: + payload = {"namespace": "ns", "image": "registry/img:new"} | overrides + return InfraConfig.model_validate(payload) + + +def _make_cluster(**overrides) -> ClusterSpec: + payload = {"name": "rc-test", "spec": _base_spec()} | overrides + return ClusterSpec.model_validate(payload) + + +class TestEnvelope: + def test_apiversion_and_kind(self) -> None: + got = build_rayjob_manifest( + _make_cluster(), _make_infra(), entrypoint="python -u foo.py" + ) + assert got["apiVersion"] == "ray.io/v1" + assert got["kind"] == "RayJob" + + def test_name_defaults_to_cluster_name(self) -> None: + got = build_rayjob_manifest( + _make_cluster(name="rc-x"), _make_infra(), entrypoint="echo hi" + ) + assert got["metadata"]["name"] == "rc-x" + + def test_name_override_wins(self) -> None: + got = build_rayjob_manifest( + _make_cluster(name="rc-x"), + _make_infra(), + entrypoint="echo", + name="sft-job", + ) + assert got["metadata"]["name"] == "sft-job" + + def test_namespace_from_infra(self) -> None: + got = build_rayjob_manifest( + _make_cluster(), + _make_infra(namespace="rl"), + entrypoint="echo", + ) + assert got["metadata"]["namespace"] == "rl" + + +class TestSpec: + def test_entrypoint_and_defaults(self) -> None: + got = build_rayjob_manifest( + _make_cluster(), _make_infra(), entrypoint="python run.py" + ) + spec = got["spec"] + assert spec["entrypoint"] == "python run.py" + assert spec["submissionMode"] == DEFAULT_SUBMISSION_MODE + assert spec["shutdownAfterJobFinishes"] is True + assert spec["ttlSecondsAfterFinished"] == 3600 + + def test_shutdown_override(self) -> None: + got = build_rayjob_manifest( + _make_cluster(), + _make_infra(), + entrypoint="x", + shutdown_after_finishes=False, + ttl_seconds_after_finished=60, + ) + assert got["spec"]["shutdownAfterJobFinishes"] is False + assert got["spec"]["ttlSecondsAfterFinished"] == 60 + + def test_ray_cluster_spec_carries_cluster_body(self) -> None: + got = build_rayjob_manifest( + _make_cluster(), _make_infra(), entrypoint="x" + ) + rcs = got["spec"]["rayClusterSpec"] + head = rcs["headGroupSpec"]["template"]["spec"]["containers"][0] + worker = rcs["workerGroupSpecs"][0]["template"]["spec"]["containers"][0] + # Image patch from infra should propagate inside rayClusterSpec. + assert head["image"] == "registry/img:new" + assert worker["image"] == "registry/img:new" + + def test_image_pull_secrets_propagate_inside_ray_cluster_spec(self) -> None: + infra = _make_infra(imagePullSecrets=["secret-a"]) + got = build_rayjob_manifest(_make_cluster(), infra, entrypoint="x") + head_pod = got["spec"]["rayClusterSpec"]["headGroupSpec"]["template"]["spec"] + assert head_pod["imagePullSecrets"] == [{"name": "secret-a"}] + + +class TestLabels: + def test_labels_merged_from_infra_cluster_and_extra(self) -> None: + cluster = _make_cluster(labels={"role": "training"}) + infra = _make_infra(labels={"team": "rl"}) + got = build_rayjob_manifest( + cluster, infra, entrypoint="x", extra_labels={"run-id": "r-1"} + ) + assert got["metadata"]["labels"] == { + "role": "training", + "team": "rl", + "run-id": "r-1", + } + + def test_extra_labels_win_on_collision(self) -> None: + cluster = _make_cluster(labels={"team": "cluster"}) + infra = _make_infra(labels={"team": "infra"}) + got = build_rayjob_manifest( + cluster, infra, entrypoint="x", extra_labels={"team": "extra"} + ) + assert got["metadata"]["labels"]["team"] == "extra" + + def test_no_labels_key_when_empty(self) -> None: + got = build_rayjob_manifest( + _make_cluster(), _make_infra(), entrypoint="x" + ) + assert "labels" not in got["metadata"] + + +class TestImmutability: + def test_input_spec_not_mutated(self) -> None: + spec = _base_spec() + original_image = spec["headGroupSpec"]["template"]["spec"]["containers"][0]["image"] + cluster = _make_cluster(spec=spec) + build_rayjob_manifest(cluster, _make_infra(), entrypoint="x") + assert ( + spec["headGroupSpec"]["template"]["spec"]["containers"][0]["image"] + == original_image + ) From 0b1f4422edad57204b2bdd9661c5e24cc7619cec Mon Sep 17 00:00:00 2001 From: Hemil Desai Date: Wed, 22 Apr 2026 15:16:06 -0700 Subject: [PATCH 33/84] infra(nrl-k8s): GB300 Qwen3-30B math recipe + single-cluster infra MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Paired example that runs the Qwen3-30B-A3B math GRPO recipe on GB300 (p6e-gb300r.36xlarge) in a single RayCluster of 8 workers × 4 GPUs (32 GPUs total, async 1-off split: 4 nodes training + 4 nodes generation inside one Ray cluster). * qwen3_30b_math_8n_4gpu.yaml — extends examples/configs/recipes/llm/performance/grpo-qwen3-30ba3b-8n4g-async-1off.yaml with 500 max_num_steps / save_period=50 / wandb tags. * qwen3_30b_math_8n_4gpu.gb300.infra.yaml — prod-mode infra. KAI scheduler with gb300-topology + nvidia.com/gpu.clique topology-required placement so the ComputeDomain NVLink channel spans all 32 GPUs in one clique. DRA ResourceClaimTemplates (compute-domain-qwen3-30b-math, roce-qwen3-30b-math) are pre-existing prereqs. Head lands on customer-cpu (48/200Gi → 60/240Gi); workers claim 120/850Gi → 132/900Gi with IPC_LOCK, NCCL_MNNVL_ENABLE=1, Lustre mounts at /opt/nemo-rl (subPath=hemild/rl-k8s) and /mnt/rl-workspace. Entrypoint flips wandb on via Hydra overrides so it works through either submission path — nrl-k8s launch/run/go (uses NRL_K8S_RUN_ID) or nrl-k8s rayjob (uses RAY_JOB_SUBMISSION_ID KubeRay injects). The OmegaConf interpolations are backslash-escaped so the $VAR text reaches the pod shell verbatim. Secrets stay via secretKeyRef (wandb-api-key). Nothing embedded. Signed-off-by: Hemil Desai Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Hemil Desai Signed-off-by: Terry Kong --- .../qwen3_30b_math_8n_4gpu.gb300.infra.yaml | 205 ++++++++++++++++++ .../examples/qwen3_30b_math_8n_4gpu.yaml | 25 +++ 2 files changed, 230 insertions(+) create mode 100644 tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml create mode 100644 tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml diff --git a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml new file mode 100644 index 00000000000..ae8fde7e358 --- /dev/null +++ b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml @@ -0,0 +1,205 @@ +# Prod-mode infra for qwen3_30b_math_8n_4gpu.yaml on GB300 (p6e-gb300r.36xlarge). +# +# Topology: 8 workers × 4 GPUs = 32 GPUs in a single RayCluster, scheduled by +# KAI as one gang. NVLink/MNNVL spans all 32 GPUs via a ComputeDomain channel, +# and NCCL rides 8× RoCE NICs per node — both are attached through Dynamic +# Resource Allocation (DRA), so ResourceClaimTemplates must already exist: +# +# compute-domain-qwen3-30b-math — ComputeDomain channel (1 per pod), +# driver compute-domain.nvidia.com, +# allocationMode Single (shared across pods). +# roce-qwen3-30b-math — 8× roce.networking.k8s.aws per pod. +# +# Submission is prod-shaped (mirrors qwen3_4b_if_single.gb300.prod.infra.yaml): +# submit.submitter portForward — Ray Job SDK via kubectl port-forward. +# launch.runMode batch — CLI returns after Ray accepts the job. +# launch.codeSource image — no working_dir upload. +# launch.codePath /opt/nemo-rl — RL repo served off the shared FSx +# Lustre PVC at subPath=hemild/rl-k8s. +# Edits on Lustre take effect without rebuilding the image. +# +# Prereqs (onboarding §3–§5 already applied in `default`): +# - rl-workspace PVC bound (shared FSx Lustre, 2.4 TiB, RWX). +# - /mnt/rl-workspace/hemild/rl-k8s contains a git checkout of +# https://github.com/NVIDIA-NeMo/RL on branch hemil/k8s-infra-cp. +# - nvcr-secret, wandb-api-key, nemo-rl-endpoint-registry all present. +# - ResourceClaimTemplates compute-domain-qwen3-30b-math and +# roce-qwen3-30b-math exist in the `default` namespace. +# - KAI topology `gb300-topology` is registered and advertises +# nvidia.com/gpu.clique as a placement key. +# +# Usage: +# # One-time per cluster: +# nrl-k8s cluster up tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml \ +# --role training +# +# # Per run — returns fast, Ray job continues in the background: +# nrl-k8s launch tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml \ +# --run-id qwen3-30b-math-$(date +%Y%m%d-%H%M%S) + +_shared: + headNodeSelector: &head_node_selector + nodeGroup: customer-cpu + workerNodeSelector: &worker_node_selector + nvidia.com/gpu.product: NVIDIA-GB300 + headEnv: &shared_head_env + - {name: HF_HOME, value: /mnt/rl-workspace/hemild/hf-cache} + - {name: HF_HUB_OFFLINE, value: "1"} + - {name: TRANSFORMERS_OFFLINE, value: "1"} + - {name: NCCL_DEBUG, value: WARN} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: + name: wandb-api-key + key: WANDB_API_KEY + workerEnv: &shared_worker_env + - {name: HF_HOME, value: /mnt/rl-workspace/hemild/hf-cache} + - {name: HF_HUB_OFFLINE, value: "1"} + - {name: TRANSFORMERS_OFFLINE, value: "1"} + - {name: NCCL_DEBUG, value: WARN} + - {name: NCCL_MNNVL_ENABLE, value: "1"} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + headResources: &head_resources + limits: {cpu: "60", memory: "240Gi"} + requests: {cpu: "48", memory: "200Gi"} + gpuWorkerResources: &gpu_worker_resources + # p6e-gb300r.36xlarge allocatable: ~139.6 CPU / ~924 GiB / 4 GPUs. + # Claim 120/132 CPU + 850/900 GiB to leave headroom for dgxc/device-plugin. + limits: + cpu: "132" + memory: "900Gi" + nvidia.com/gpu: "4" + requests: + cpu: "120" + memory: "850Gi" + nvidia.com/gpu: "4" + claims: + - {name: compute-domain-channel} + - {name: roce-channel} + gpuHeadPorts: &gpu_head_ports + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + # Lustre PVC at /opt/nemo-rl (subPath hemild/rl-k8s) for code; at + # /mnt/rl-workspace (no subPath) for datasets, checkpoints, HF cache. + codeMounts: &code_mounts + - {mountPath: /dev/shm, name: dshm} + - {mountPath: /opt/nemo-rl, name: rl-workspace, subPath: hemild/rl-k8s} + - {mountPath: /mnt/rl-workspace, name: rl-workspace} + headVolumes: &head_volumes + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 32Gi} + - name: rl-workspace + persistentVolumeClaim: {claimName: rl-workspace} + workerVolumes: &worker_volumes + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 64Gi} + - name: rl-workspace + persistentVolumeClaim: {claimName: rl-workspace} + +# namespace auto-inferred from kube context (default). +image: nvcr.io/nvidian/nemo-rl:nightly +imagePullSecrets: [nvcr-secret] +serviceAccount: nemo-rl-endpoint-registry + +submit: + submitter: portForward + +launch: + mode: attach + runMode: batch + codeSource: image + codePath: /opt/nemo-rl + attach: + training: raycluster-qwen3-30b-math-gb300 + peerWatcher: false + env: {} + # Runs inside the training head pod under Ray's Job SDK. Megatron's optimizer + # config doesn't carry `foreach/fused`, so the deletions used in the 4B gym + # example don't apply here. + # + # Hydra overrides flip wandb on at runtime so the recipe on disk doesn't + # need to hardcode `logger.wandb_enabled=true`. RUN_ID is derived from the + # KubeRay submission id (RayJob mode) or NRL_K8S_RUN_ID (launch/run/go) + # so each submission gets a distinct wandb run name; falls back to a + # timestamp when neither is set. Backslashes escape OmegaConf + # interpolation so the `$VAR` text reaches the pod shell verbatim. + entrypoint: | + set -eu + cd /opt/nemo-rl + export RAY_enable_infeasible_task_early_exit=true + export PYTHONPATH=.:3rdparty/Megatron-LM-workspace/Megatron-LM + RUN_ID="\${RAY_JOB_SUBMISSION_ID:-\${NRL_K8S_RUN_ID:-$(date -u +%Y%m%d-%H%M%S)}}" + + python -u examples/run_grpo.py \ + --config tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml \ + logger.wandb_enabled=true \ + logger.wandb.project=nemorl-single-k8s \ + "logger.wandb.name=qwen3-30b-math-gb300-\${RUN_ID}" + +clusters: + training: + name: raycluster-qwen3-30b-math-gb300 + labels: + disagg.nemo-rl/cluster: qwen3-30b-math-gb300 + disagg.nemo-rl/run: qwen3-30b-math-gb300 + kai.scheduler/queue: default-queue + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: + dashboard-host: "0.0.0.0" + no-monitor: "true" + num-gpus: "0" + object-store-memory: "200000000" + template: + spec: + schedulerName: kai-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *head_node_selector + containers: + - name: ray-head + env: *shared_head_env + resources: *head_resources + ports: *gpu_head_ports + volumeMounts: *code_mounts + volumes: *head_volumes + workerGroupSpecs: + - groupName: gpu-workers + replicas: 8 + minReplicas: 8 + maxReplicas: 8 + numOfHosts: 1 + rayStartParams: + num-gpus: "4" + object-store-memory: "200000000" + template: + metadata: + annotations: + # KAI co-schedules the 8 workers in a single gb300-topology + # domain so the ComputeDomain NVLink channel spans all 32 GPUs. + kai.scheduler/topology: gb300-topology + kai.scheduler/topology-required-placement: nvidia.com/gpu.clique + spec: + schedulerName: kai-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *worker_node_selector + tolerations: + - operator: Exists + resourceClaims: + - name: compute-domain-channel + resourceClaimTemplateName: compute-domain-qwen3-30b-math + - name: roce-channel + resourceClaimTemplateName: roce-qwen3-30b-math + containers: + - name: ray-worker + env: *shared_worker_env + resources: *gpu_worker_resources + securityContext: + capabilities: + add: [IPC_LOCK] + volumeMounts: *code_mounts + volumes: *worker_volumes diff --git a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml new file mode 100644 index 00000000000..86d4d3fc26d --- /dev/null +++ b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml @@ -0,0 +1,25 @@ +# Qwen3-30B-A3B GRPO on math — **single RayCluster, 8 nodes × 4 GPUs (GB300)**. +# +# Extends the performance async-1off recipe (training + generation inside one +# Ray cluster, non-colocated split: 4 nodes training, 4 nodes generation). +# The 32 workers are a single pod-group on GB300 so NVLink spans all 32 GPUs +# via a ComputeDomain channel claim (see the paired infra file). +# +# Pair with qwen3_30b_math_8n_4gpu.gb300.infra.yaml. +defaults: ../../../examples/configs/recipes/llm/performance/grpo-qwen3-30ba3b-8n4g-async-1off.yaml + +cluster: + gpus_per_node: 4 + num_nodes: 8 +grpo: + max_num_steps: 500 + val_period: 50 +checkpointing: + save_period: 50 +logger: + wandb_enabled: true + wandb: + project: nemorl-single-k8s + name: qwen3-30b-math-8n-4gpu-gb300 + tensorboard_enabled: true + monitor_gpus: true From c3f34ae9327fcd1747691c9d98c192f4d88ad49d Mon Sep 17 00:00:00 2001 From: Hemil Desai Date: Wed, 22 Apr 2026 15:51:08 -0700 Subject: [PATCH 34/84] docs(nrl-k8s): add launch-nemo-rl Claude skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Project-scoped skill at tools/nrl_k8s/.claude/skills/launch-nemo-rl/ capturing the playbook for running NeMo-RL recipes on a Kubernetes cluster via the nrl-k8s CLI. Covers: * Mode decision tree: long-lived `nrl-k8s run` vs ephemeral `nrl-k8s run --rayjob` (auto-teardown). * Recipe + infra pairing, the three-dimensional flag model (mode / submitter / code-source) and per-mode flag matrix. * Per-profile concerns: per-node GPU count matching the recipe, head vs worker node selectors, KAI scheduler + topology annotations, DRA ResourceClaimTemplate prereqs, shared-filesystem mount conventions, secret references. * Hydra override pattern on infra.launch.entrypoint for iterating without touching the shared filesystem — including the OmegaConf `\${VAR}` escape so Bash sees `${VAR:-default}` literally. * End-to-end workflows (one-shot rayjob, dev loop, disagg bring-up, cluster-only lifecycle), monitoring via `nrl-k8s job logs` and the dashboard `/api/jobs//logs` endpoint, stopping things. * RayJob teardown verification + "done" checklist (including sharing the wandb URL from the driver log). * Common gotchas: OmegaConf interpolation, Megatron optimizer kwargs, shared-filesystem vs git divergence, kuberay-injected pod fields, dashboard symlink fix, kubectl exec restrictions. Auto-invocable via the `description` + `when_to_use` frontmatter on natural phrasings ("launch on the cluster", "resubmit as rayjob", etc.) or explicitly via `/launch-nemo-rl`. No secrets embedded. Signed-off-by: Hemil Desai Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Hemil Desai Signed-off-by: Terry Kong --- .../.claude/skills/launch-nemo-rl/SKILL.md | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 tools/nrl_k8s/.claude/skills/launch-nemo-rl/SKILL.md diff --git a/tools/nrl_k8s/.claude/skills/launch-nemo-rl/SKILL.md b/tools/nrl_k8s/.claude/skills/launch-nemo-rl/SKILL.md new file mode 100644 index 00000000000..6ccd89a1146 --- /dev/null +++ b/tools/nrl_k8s/.claude/skills/launch-nemo-rl/SKILL.md @@ -0,0 +1,234 @@ +--- +name: launch-nemo-rl +description: Playbook for launching, monitoring, stopping, and debugging NeMo-RL recipes on a Kubernetes cluster via the nrl-k8s CLI. Use when asked to run/launch/submit/test a recipe on k8s, bring up or tear down a RayCluster, choose between long-lived (default `nrl-k8s run`) and ephemeral (`nrl-k8s run --rayjob`, auto-teardown) modes, iterate on an existing run, debug a hung or failed training job, or structure a new recipe+infra pair for a new hardware profile. +when_to_use: "run this recipe on k8s", "launch on the cluster", "submit a training job", "tear down the cluster", "resubmit as rayjob", "why is the run stuck", "how do I get logs for job X", "bring the cluster back up". +allowed-tools: Bash Read Grep Glob Edit Write +--- + +# launch-nemo-rl — running NeMo-RL recipes on Kubernetes via nrl-k8s + +This is the playbook for the `nrl-k8s` CLI at `tools/nrl_k8s/`. Follow it when the user asks to launch / iterate / debug a NeMo-RL recipe on a Kubernetes cluster. Verify current state (`kubectl`, `git log`, the recipe + infra files) before acting — the cluster is shared and the cost of a wrong action is high. + +## 1. One command, two modes + +There is a single top-level submission command: **`nrl-k8s run`**. It has two lifecycle modes. + +| Mode | Invocation | When to use | Cluster after? | +| :----------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------- | +| Long-lived (default) | `nrl-k8s run` | Dev loop. Reuses a matching live cluster, applies if absent, warns + reuses on drift (pass `--recreate` to replace). Then submits daemons and training. First-choice for iteration. | Yes | +| Ephemeral | `nrl-k8s run --rayjob` | One-shot. KubeRay applies a RayJob, runs, tears the cluster down. Best for paper / eval runs where you don't plan to resubmit. | No (auto) | + +Ask: *Do I need this cluster after the run?* If no, use `--rayjob`. If yes, plain `run`. + +The rest of the CLI is observability / stage-by-stage control: + +| Command | Purpose | +| :---------------------- | :---------------------------------------------------------------------------------------------- | +| `nrl-k8s check` | Validate a recipe + infra pair; optionally write the fully-resolved manifests (`-o`). | +| `nrl-k8s status` | Per-role RayCluster state, head pod phase, worker pod phases, daemon job status. | +| `nrl-k8s cluster up/down/list/dashboard` | Manage RayClusters independently of a run (e.g. render a manifest with `--dry-run`). | +| `nrl-k8s job list/logs/stop` | Observability over Ray Jobs already submitted to a role's cluster. | +| `nrl-k8s logs` | Tail a role's pod / daemon logs without needing a submission id. | + +## 2. Recipe + infra pair + +Every launch takes two files. Pass the infra with `--infra`, not merged inline: + +``` +nrl-k8s run tools/nrl_k8s/examples/.yaml \ + --infra tools/nrl_k8s/examples/..infra.yaml +``` + +- **Recipe** (e.g. `qwen3_30b_math_8n_4gpu.yaml`) — NeMo-RL config: model, GRPO/SFT knobs, `cluster.{gpus_per_node,num_nodes}`. Uses `defaults:` to inherit from `examples/configs/recipes/llm/...`. +- **Infra** (e.g. `*..infra.yaml`) — K8s/Ray shape: namespace, image, service account, RayCluster spec, `submit.submitter`, `launch.{mode,codeSource,codePath,entrypoint}`. Pair names follow `.[.prod].infra.yaml` where `` names the hardware target (e.g. `gb300`). + +Example pairs in `tools/nrl_k8s/examples/` — read the neighbouring files to see the current conventions for the target profile. + +## 3. Long-lived mode flags + +Three independent dimensions. `--mode` is a macro that picks defaults; individual flags override it. + +``` +--mode interactive → --submitter portForward --code-source upload (tails logs) +--mode batch → --submitter exec --code-source image (returns after nohup) +``` + +- **Submitter**: `portForward` uses `kubectl port-forward` + Ray Job SDK (gets a `submission_id` the dashboard tracks). `exec` uses `kubectl exec` + `nohup` on the head pod (no submission_id; driver appears as `type=DRIVER` in the dashboard). +- **Code source**: `upload` stages a working_dir from the laptop (Ray 100 MiB cap). `image` / `lustre` expect code on the pod's filesystem — paired with `--code-path` (typically `/opt/nemo-rl`), which is a subPath of the shared-filesystem PVC mount in the standard infra examples. +- **Wait**: `--wait` tails logs until terminal; `--no-wait` returns as soon as the driver is running. + +Other long-lived-only flags: + +- `--replace` — stop any running training / daemon job before submitting new ones (suffixes daemon submissionIds with a timestamp so Ray accepts the resubmit). +- `--recreate` — delete + re-apply a RayCluster whose live spec has drifted from the rendered manifest (default is warn + reuse). +- `--skip-daemons` — bring up all declared clusters but only submit training. Use on disagg recipes where gym/generation are already healthy. + +Gotcha: on infra where the entrypoint does `cd /opt/nemo-rl` (or another in-image / Lustre path) and loads the recipe from there, **`--code-source upload` does NOT override the recipe on the pod** — the uploaded working_dir sits in `/tmp/ray/...` but the entrypoint `cd`s away from it. To actually test a local recipe change, either sync your edits to the shared filesystem mounted into the pods or flip the Hydra overrides in the entrypoint. + +## 4. Ephemeral mode flags (`--rayjob`) + +When `--rayjob` is set, `run` branches into the RayJob code path. Relevant flags: + +- `--rayjob-name NAME` — RayJob metadata name (defaults to the training cluster name). +- `--shutdown / --no-shutdown` — default `true`: KubeRay deletes the RayCluster once the Ray Job reaches a terminal state. +- `--ttl SECONDS` — default 3600s: keep the RayJob object around after the run finishes for post-mortem log access. +- `--wait / --no-wait` — default `wait`: poll `jobDeploymentStatus` until Complete/Failed. `--no-wait` returns as soon as the RayJob is applied. +- `--timeout SECONDS` — default 86400s (24h): bound the `--wait` poll. +- `--dry-run` — render the RayJob manifest and print it; do not apply. + +`--replace` / `--recreate` / `--skip-daemons` are silently ignored in `--rayjob` mode (KubeRay owns lifecycle). + +## 5. Iterating on a config without touching the shared filesystem + +When the recipe on the pod filesystem has the wrong value for your experiment, use Hydra overrides on the entrypoint instead of forking the recipe. Pattern: + +```yaml +entrypoint: | + set -eu + cd /opt/nemo-rl + RUN_ID="\${RAY_JOB_SUBMISSION_ID:-\${NRL_K8S_RUN_ID:-$(date -u +%Y%m%d-%H%M%S)}}" + python -u examples/run_grpo.py \ + --config tools/nrl_k8s/examples/.yaml \ + logger.wandb_enabled=true \ + logger.wandb.project= \ + "logger.wandb.name=-\${RUN_ID}" +``` + +**Escape `${…}`** with a backslash. OmegaConf otherwise interprets it as interpolation and errors on shell-style `${VAR:-default}`. `RUN_ID` resolves to `RAY_JOB_SUBMISSION_ID` (injected by KubeRay in rayjob mode) → `NRL_K8S_RUN_ID` (injected by the CLI in long-lived mode) → local timestamp — so the name is unique across either path. + +## 6. Per-profile concerns (hardware + scheduler + DRA) + +Every infra YAML encodes a hardware/scheduler profile. The concrete examples in `tools/nrl_k8s/examples/` are authoritative for the profiles they target — read the neighbouring infra file before writing a new one. Things that commonly vary: + +- **Per-node GPUs** (e.g. 4 vs 8) — must match `cluster.gpus_per_node` in the recipe, otherwise workers stay `Pending`. +- **Node selectors** — head pods usually land on a CPU-only node pool; GPU workers match on `nvidia.com/gpu.product` or a node-group label. +- **Scheduler** — KAI (`schedulerName: kai-scheduler` + `kai.scheduler/queue` label) with topology annotations (`kai.scheduler/topology`, `kai.scheduler/topology-required-placement`) gang-schedules workers into one clique. Without it, pods may land on different racks and NVLink/RoCE won't span them. +- **DRA claims** — ComputeDomain + RoCE are attached via `resourceClaims` referencing `ResourceClaimTemplate`s that must pre-exist in the namespace. Missing templates = `RayCluster` Pending forever. +- **Secrets** — always via `secretKeyRef` (`wandb-api-key`, image pull secret). Never embed. +- **Shared filesystem mounts** — typically a Lustre PVC mounted twice: once at the code path (e.g. `/opt/nemo-rl` with a user-scoped `subPath`) and once at a workspace root (e.g. `/mnt/rl-workspace`) for datasets, HF cache, and checkpoints. + +Before applying an infra, verify prereqs exist in the target namespace: + +```bash +kubectl get resourceclaimtemplate +kubectl get pvc +kubectl get secret +kubectl get sa +``` + +See `tools/nrl_k8s/docs/onboarding.md` for ComputeDomain CR + RoCE ResourceClaimTemplate shapes and the full onboarding checklist for a new namespace. + +## 7. End-to-end workflows + +### 7a. Fresh one-shot run (rayjob) +```bash +# From the NeMo-RL repo root: +nrl-k8s check --infra # validate first +nrl-k8s run --infra --rayjob --dry-run # render RayJob manifest +nrl-k8s run --infra --rayjob --no-wait # apply, returns fast +``` + +Watch status + teardown (works even after your laptop disconnects because KubeRay owns the lifecycle): +```bash +kubectl get rayjob -n default -w +kubectl get raycluster -n default # empty = teardown succeeded +``` + +### 7b. Dev loop (long-lived) +```bash +nrl-k8s run --infra --run-id $(date +%Y%m%d-%H%M%S) +# Edits in the recipe? Just re-run — reuses the live cluster. +# Pod spec changed? Add --recreate to delete + re-apply. +# Disagg recipe with gym/gen already healthy? --skip-daemons. +``` + +### 7c. First-time disaggregated bring-up +```bash +nrl-k8s run --infra --mode batch --code-source image +``` + +### 7d. Cluster-only lifecycle +```bash +nrl-k8s cluster up --infra --role training --wait +nrl-k8s cluster up --infra --role training --dry-run # render manifest +nrl-k8s cluster down --infra --role training --wait +nrl-k8s cluster list -n default +nrl-k8s cluster dashboard # port-forward + browser +``` + +## 8. Monitoring a run + +```bash +# Status +nrl-k8s status --infra +kubectl get rayjob,raycluster -n default + +# Follow the driver +nrl-k8s job list --infra --role training +nrl-k8s job logs --infra --role training -f +``` + +When the `nrl-k8s job logs -f` subprocess dies (`kubectl port-forward` i/o timeout after ~15 min idle), just re-run it. The training job keeps going. + +To fetch driver logs for a terminal job (SUCCEEDED/FAILED) or a RayJob via the dashboard API: +```bash +RC=$(kubectl get rayjob -n default -o jsonpath='{.status.rayClusterName}') +kubectl port-forward -n default svc/${RC}-head-svc 18266:8265 & +curl -s http://localhost:18266/api/jobs/ # lists jobs, find submission_id +curl -s "http://localhost:18266/api/jobs//logs" # full driver log +``` + +`type=DRIVER` with `submission_id=null` means an exec-submitter run (no dashboard log endpoint — use `nrl-k8s job logs` instead). `type=SUBMISSION` has `submission_id` set and `/api/jobs//logs` works. + +Wandb URL appears in the driver log on the first `wandb.init` call; grep `grep -oE 'https://wandb\.ai/[A-Za-z0-9_./-]+'`. + +## 9. Stopping things + +| What to stop | Command | +| :------------------------------- | :----------------------------------------------------------------------------------- | +| One training run | `nrl-k8s job stop --infra --role training` | +| All running Ray jobs on a cluster (+ submit new) | `nrl-k8s run --infra --replace` | +| A long-lived RayCluster | `nrl-k8s cluster down --infra --role training --wait` | +| A RayJob (ephemeral) | `kubectl delete rayjob -n default` — only if `shutdownAfterJobFinishes` didn't fire | + +Confirm before deleting shared infra. The cost of `cluster down` on someone else's cluster is high. + +## 10. Verifying RayJob teardown + +After a `run --rayjob` completes with `--shutdown` (default), KubeRay should delete the RayCluster: + +```bash +kubectl get rayjob -n default # jobDeploymentStatus = Complete +kubectl get raycluster -n default | grep # no output = torn down +``` + +The RayJob object itself sticks around for `--ttl` seconds (default 3600s) so you can still fetch logs. + +## 11. Common gotchas + +- **OmegaConf interpolation** eats `${VAR}` in recipe/infra YAML. Escape shell variables with `\${VAR}` so OmegaConf passes them through to the pod shell verbatim. +- **Megatron optimizer configs** don't carry `foreach` / `fused`. Overrides like `~policy.optimizer.kwargs.foreach ~policy.optimizer.kwargs.fused` (valid for DTensor configs) break on Megatron recipes. Omit them for Megatron. +- **DTensor vs Megatron** — MoE recipes typically use `megatron_cfg.enabled=true`; ensure `dtensor_cfg.enabled=false` in inherited defaults. +- **Shared filesystem vs git divergence** — `codeSource: image|lustre` reads from the pod filesystem. If your local edits aren't on the shared filesystem the pods mount, the run is testing the on-disk version, not yours. Either sync via a helper pod (head pod exec is often blocked) or override via Hydra flags. +- **Ephemeral-storage + readinessProbe** are injected by kuberay/CDI webhooks at pod-apply time. Do NOT add them to the inline RayCluster spec. +- **Node taints** vary per cluster. `tolerations: [{operator: Exists}]` on workers is defensive and worth keeping. +- **Dashboard blank page** — Ray 2.52 installs dashboard assets as symlinks by default; `nrl-k8s cluster dashboard ` auto-reinstalls `ray[default] --link-mode=copy` to fix it. Bake `ENV UV_LINK_MODE=copy` in the image to avoid this entirely. +- **`kubectl exec` is usually blocked** in automation — route around with `kubectl get ... -o yaml`, `kubectl logs`, and `kubectl port-forward` + Ray dashboard APIs. + +## 12. Checklist before calling a run "done" + +Before reporting a launch as successful, verify: + +1. `kubectl get rayjob/raycluster -n default` shows the expected objects. +2. `nrl-k8s job list` (or `curl /api/jobs/`) shows the job in `RUNNING` / `SUCCEEDED`. +3. Driver log contains `wandb.ai//runs/` (if wandb is enabled) — share the URL with the user. +4. At least one `Processed prompts: 100%` line appears (confirms generation is wired). +5. For `--rayjob` mode only: after `jobDeploymentStatus=Complete`, confirm `kubectl get raycluster | grep ` is empty (teardown worked). + +## 13. Where things live in the repo + +- CLI code: `tools/nrl_k8s/src/nrl_k8s/` (`cli.py`, `orchestrate.py`, `manifest.py`, `rayjob.py`, `k8s.py`, `submitters/`, `schema.py`). +- Tests: `tools/nrl_k8s/tests/unit/` — run with `uv run --active pytest tests/unit -x` from `tools/nrl_k8s/`. +- Recipe + infra examples: `tools/nrl_k8s/examples/`. +- Onboarding + recipes docs: `tools/nrl_k8s/docs/`. +- Base recipes this tool wraps: `examples/configs/recipes/llm/…` and `examples/nemo_gym/…`. From 5a3f4aaf32938a1735e1c04a3a008a960e962c6b Mon Sep 17 00:00:00 2001 From: Hemil Desai Date: Wed, 22 Apr 2026 15:51:30 -0700 Subject: [PATCH 35/84] refactor(nrl-k8s): collapse launch/run/go/rayjob into one `run` command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single submission command with two lifecycle modes: * `nrl-k8s run` (default) — long-lived, idempotent. For each declared role, reuse the live RayCluster when its spec matches, apply when absent, warn + reuse on drift (`--recreate` to replace). Then submit daemons and training. Keeps the macro (`--mode interactive|batch`) + transport (`--submitter`) + code-source (`--code-source`) knobs; adds `--replace`, `--recreate`, `--skip-daemons` from the old `go`. * `nrl-k8s run --rayjob` — ephemeral. KubeRay applies a RayJob, runs, tears the cluster down. Same flags the standalone `rayjob` had (`--rayjob-name`, `--shutdown/--no-shutdown`, `--ttl`, `--timeout`, `--dry-run`), scoped under `--rayjob`. Drops: * `launch` — subsumed by `run` (long-lived path without `--skip-daemons`). * `go` — subsumed by `run` (now the default idempotent behaviour). * `rayjob` — subsumed by `run --rayjob`. Internals: * `orchestrate.run` is now the idempotent go-style pipeline; the old `go` body was renamed into it and `ensure_cluster` + drift diff stay. * `cli.run` dispatches to a `_run_rayjob` helper when `--rayjob` is set. * Tests renamed: `TestGo` → `TestRun`, `TestGoCommand` → `TestRunCommand`; `TestRayJob` now invokes `run --rayjob`. 161 passed. * Schema + CLI error strings + example YAML usage comments updated to reference `nrl-k8s run` / `nrl-k8s run --rayjob`. Signed-off-by: Hemil Desai Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Hemil Desai Signed-off-by: Terry Kong --- .../qwen3_30b_math_8n_4gpu.gb300.infra.yaml | 14 +- .../qwen3_4b_if_full_disagg.infra.yaml | 2 +- .../qwen3_4b_if_gym_disagg.prod.infra.yaml | 7 +- .../qwen3_4b_if_single.gb300.prod.infra.yaml | 14 +- tools/nrl_k8s/src/nrl_k8s/cli.py | 369 ++++++------------ tools/nrl_k8s/src/nrl_k8s/orchestrate.py | 39 +- tools/nrl_k8s/src/nrl_k8s/schema.py | 2 +- tools/nrl_k8s/tests/unit/test_cli.py | 22 +- tools/nrl_k8s/tests/unit/test_orchestrate.py | 8 +- 9 files changed, 162 insertions(+), 315 deletions(-) diff --git a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml index ae8fde7e358..ebb201511b6 100644 --- a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml @@ -29,15 +29,15 @@ # nvidia.com/gpu.clique as a placement key. # # Usage: -# # One-time per cluster: -# nrl-k8s cluster up tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml \ -# --role training -# -# # Per run — returns fast, Ray job continues in the background: -# nrl-k8s launch tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml \ +# # Long-lived cluster — idempotent; reuses the live cluster if it +# # already matches, applies if absent. Submits training per invocation. +# nrl-k8s run tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml \ # --infra tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml \ # --run-id qwen3-30b-math-$(date +%Y%m%d-%H%M%S) +# +# # One-shot with auto-teardown (ephemeral RayCluster, no cleanup needed): +# nrl-k8s rayjob tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml _shared: headNodeSelector: &head_node_selector diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml index b0c06dd75b8..de29688e6b2 100644 --- a/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml @@ -328,7 +328,7 @@ clusters: labels: disagg.nemo-rl/cluster: rl-qwen3-4b disagg.nemo-rl/run: qwen3-4b-gym - # No daemon: waits for `nrl-k8s launch` / `nrl-k8s run`. + # No daemon: waits for `nrl-k8s run` / `nrl-k8s rayjob`. spec: rayVersion: "2.52.0" headGroupSpec: diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml index 22e046ae4ef..8dd284654f1 100644 --- a/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml @@ -15,8 +15,11 @@ # --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ # --role training # -# # Researcher, per run — returns in seconds, laptop disconnectable. -# nrl-k8s launch tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ +# # Researcher, per run — idempotent; reuses live clusters if matching, +# # submits training against the training cluster. Returns in seconds, +# # laptop disconnectable. Use --skip-daemons after first bring-up if +# # gym/generation are already healthy. +# nrl-k8s run tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ # --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ # --run-id qwen3-4b-gym-disagg-$(date +%Y%m%d-%H%M%S) # diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml index e514e0d6a69..ae3e3f8610c 100644 --- a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml @@ -22,15 +22,15 @@ # - nvcr-secret, wandb-api-key, nemo-rl-endpoint-registry all present # # Usage: -# # One-time per cluster: -# nrl-k8s cluster up tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml \ -# --role training -# -# # Per run — returns fast, Ray job continues in the background: -# nrl-k8s launch tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ +# # Long-lived cluster — idempotent; reuses the live cluster if it +# # already matches, applies if absent. Submits training per invocation. +# nrl-k8s run tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ # --infra tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml \ # --run-id single-if-$(date +%Y%m%d-%H%M%S) +# +# # One-shot with auto-teardown (ephemeral RayCluster): +# nrl-k8s rayjob tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml _shared: nodeSelector: &shared_node_selector diff --git a/tools/nrl_k8s/src/nrl_k8s/cli.py b/tools/nrl_k8s/src/nrl_k8s/cli.py index d5b9b9c66fb..1e5bf5bf0b8 100644 --- a/tools/nrl_k8s/src/nrl_k8s/cli.py +++ b/tools/nrl_k8s/src/nrl_k8s/cli.py @@ -358,186 +358,70 @@ def validate(ctx, recipe, overrides, infra_path) -> None: @click.option( "--replace", is_flag=True, - help="Stop any running training job on the cluster before submitting.", + help="Long-lived mode only: stop any running daemon/training job before " + "submitting new ones.", ) -@_mode_options -def launch( - recipe: Path, - overrides: tuple[str, ...], - infra_path: Path | None, - repo_root: Path, - replace: bool, - cli_mode: str | None, - cli_submitter: str | None, - cli_code_source: str | None, - cli_code_path: str | None, - cli_run_id: str | None, - cli_wait: bool | None, -) -> None: - """Submit a training job against an already-up training cluster. - - ``--mode interactive`` (default) uses port-forward + working_dir - upload and tails logs. ``--mode batch`` uses kubectl exec + in-image - code, returns as soon as the driver is running via nohup, and the - laptop can disconnect. - - ``--replace`` stops any RUNNING Ray Job on the training cluster first - so the new submission doesn't queue behind GPU-holding stragglers. - """ - from . import orchestrate - from . import submit as submit_mod - - loaded = _load_or_exit(recipe, overrides, infra_path) - if not submit_mod.is_in_cluster(): - _preflight_or_exit(loaded.infra.namespace) - if not loaded.infra.launch.entrypoint: - _cli_error( - "infra.launch.entrypoint is empty", - hint="launch command requires infra.launch.entrypoint; see docs/recipes.md", - ) - - mode, submitter, code_src, no_wait = _resolve_mode_defaults( - cli_mode=cli_mode, - infra_mode=loaded.infra.launch.runMode, - cli_submitter=cli_submitter, - cli_code_source=cli_code_source, - cli_wait=cli_wait, - ) - _apply_mode_overrides(loaded, submitter=submitter, code_source=code_src, code_path=cli_code_path) - click.echo( - f"[launch] mode={mode.value} submitter={submitter.value} " - f"code_source={code_src.value} no_wait={no_wait}", - err=True, - ) - - try: - result = orchestrate.submit_training( - loaded, - log=click.echo, - repo_root=repo_root.resolve(), - replace=replace, - run_id=cli_run_id, - ) - except Exception as exc: # noqa: BLE001 - _explain_and_exit(exc, context="launch failed") - - _emit_handle(result.handle) - if not no_wait: - _follow_handle(result.handle) - - -@main.command() -@click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) -@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) -@_INFRA_OPTION @click.option( - "--repo-root", - type=click.Path(exists=True, file_okay=False, path_type=Path), - default=Path.cwd(), - show_default="cwd", - help="NeMo-RL repo root used to source files for the working_dir upload.", + "--recreate", + is_flag=True, + help="Long-lived mode only: delete + re-apply any RayCluster whose live " + "spec has drifted from the rendered manifest.", ) @click.option( - "--replace", + "--skip-daemons", is_flag=True, - help="Stop any running daemon/training job before submitting new ones.", + help="Long-lived mode only: bring up every declared cluster but only " + "submit training — skip daemons on gym/generation roles.", ) -@_mode_options -def run( - recipe: Path, - overrides: tuple[str, ...], - infra_path: Path | None, - repo_root: Path, - replace: bool, - cli_mode: str | None, - cli_submitter: str | None, - cli_code_source: str | None, - cli_code_path: str | None, - cli_run_id: str | None, - cli_wait: bool | None, -) -> None: - """Bring up every cluster + daemon declared in the recipe, then submit training. - - One command takes a recipe from zero to a running job: apply each - RayCluster, submit its daemon (for generation/gym), then submit the - training entrypoint against the training cluster. - - ``--mode batch`` is the production shape: bring up the training - cluster, exec into its head, start the entrypoint under nohup, and - return. Pair with ``--code-source image`` or ``--code-source lustre`` - to skip the laptop-side working_dir upload entirely. - - ``--replace`` stops any previous RUNNING instance of a daemon or - training job before submitting (and suffixes daemon submissionIds - with a timestamp so Ray accepts the resubmit). - """ - from . import orchestrate - from . import submit as submit_mod - - loaded = _load_or_exit(recipe, overrides, infra_path) - if not submit_mod.is_in_cluster(): - _preflight_or_exit(loaded.infra.namespace) - - mode, submitter, code_src, no_wait = _resolve_mode_defaults( - cli_mode=cli_mode, - infra_mode=loaded.infra.launch.runMode, - cli_submitter=cli_submitter, - cli_code_source=cli_code_source, - cli_wait=cli_wait, - ) - _apply_mode_overrides(loaded, submitter=submitter, code_source=code_src, code_path=cli_code_path) - click.echo( - f"[run] mode={mode.value} submitter={submitter.value} " - f"code_source={code_src.value} no_wait={no_wait}", - err=True, - ) - - try: - result = orchestrate.run( - loaded, - log=click.echo, - repo_root=repo_root.resolve(), - replace=replace, - run_id=cli_run_id, - ) - except Exception as exc: # noqa: BLE001 - _explain_and_exit(exc, context="run failed") - - _emit_handle(result.handle) - if not no_wait: - _follow_handle(result.handle) - - -@main.command() -@click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) -@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) -@_INFRA_OPTION @click.option( - "--repo-root", - type=click.Path(exists=True, file_okay=False, path_type=Path), - default=Path.cwd(), - show_default="cwd", - help="NeMo-RL repo root used to source files for the working_dir upload.", + "--rayjob", + "as_rayjob", + is_flag=True, + help="Submit as an ephemeral KubeRay RayJob (auto-teardown) instead of " + "attaching to a long-lived RayCluster. Ignores --replace/--recreate/" + "--skip-daemons (they're not applicable).", ) @click.option( - "--replace", - is_flag=True, - help="Stop any running daemon/training job before submitting new ones.", + "--rayjob-name", + "rayjob_name", + type=str, + default=None, + help="[--rayjob only] RayJob metadata name. Defaults to the training " + "cluster name.", ) @click.option( - "--recreate", - is_flag=True, - help="Delete + re-apply any RayCluster whose live spec has drifted from " - "the rendered manifest. Default is to warn and reuse as-is.", + "--shutdown/--no-shutdown", + "rayjob_shutdown", + default=True, + show_default=True, + help="[--rayjob only] Delete the ephemeral RayCluster once the Ray Job " + "reaches a terminal state (KubeRay's shutdownAfterJobFinishes).", ) @click.option( - "--skip-daemons", + "--ttl", + "rayjob_ttl", + type=int, + default=3600, + show_default=True, + help="[--rayjob only] Seconds to keep the RayJob object after the run " + "finishes (ttlSecondsAfterFinished). Useful for post-mortem log access.", +) +@click.option( + "--timeout", + "rayjob_timeout", + type=int, + default=86400, + show_default=True, + help="[--rayjob only] Seconds to wait for the RayJob to reach a terminal " + "state when --wait is set.", +) +@click.option( + "--dry-run", is_flag=True, - help="Bring up every declared cluster but only submit training — skip " - "daemons on gym/generation roles.", + help="[--rayjob only] Render the RayJob manifest and print it; do not apply.", ) @_mode_options -def go( +def run( recipe: Path, overrides: tuple[str, ...], infra_path: Path | None, @@ -545,6 +429,12 @@ def go( replace: bool, recreate: bool, skip_daemons: bool, + as_rayjob: bool, + rayjob_name: str | None, + rayjob_shutdown: bool, + rayjob_ttl: int, + rayjob_timeout: int, + dry_run: bool, cli_mode: str | None, cli_submitter: str | None, cli_code_source: str | None, @@ -552,22 +442,54 @@ def go( cli_run_id: str | None, cli_wait: bool | None, ) -> None: - """Idempotent ``run``: reuse matching clusters, then launch training. - - For each role declared in the recipe, reuse the live RayCluster when - its spec matches the rendered manifest, apply when it is absent, and - warn + reuse when it has drifted (pass ``--recreate`` to delete + - re-apply). Then submit daemons and the training entrypoint, same as - :command:`launch`. - - Use ``--skip-daemons`` on a disaggregated recipe when the gym / - generation daemons are already healthy and you only want to re-submit - training. + """Submit a recipe to the cluster. Long-lived by default, ephemeral with ``--rayjob``. + + **Long-lived mode (default)** — idempotent: for each declared role, + reuse the live RayCluster when its spec matches the rendered manifest, + apply when it is absent, warn + reuse on drift (pass ``--recreate`` to + delete + re-apply). Then submit daemons and the training entrypoint. + Cluster stays up for subsequent ``nrl-k8s run`` invocations. + ``--mode interactive`` (default) uses port-forward + working_dir upload + and tails logs; ``--mode batch`` uses kubectl exec + in-image code and + returns as soon as the driver is running via nohup. + + **Ephemeral mode (``--rayjob``)** — submits the recipe as a KubeRay + RayJob. KubeRay creates the RayCluster, submits + ``infra.launch.entrypoint`` over the dashboard HTTP API, polls until + the driver is terminal, then tears the cluster down. + ``shutdownAfterJobFinishes=true`` by default. Pass ``--no-wait`` to + return as soon as the RayJob is applied, ``--dry-run`` to render the + manifest without applying. """ from . import orchestrate from . import submit as submit_mod loaded = _load_or_exit(recipe, overrides, infra_path) + if not loaded.infra.launch.entrypoint: + _cli_error( + "infra.launch.entrypoint is empty", + hint="`nrl-k8s run` requires infra.launch.entrypoint; see docs/recipes.md", + ) + + if as_rayjob: + _run_rayjob( + loaded, + recipe=recipe, + name=rayjob_name, + shutdown_after=rayjob_shutdown, + ttl_seconds=rayjob_ttl, + timeout_s=rayjob_timeout, + dry_run=dry_run, + cli_wait=cli_wait, + ) + return + + if dry_run: + _cli_error( + "--dry-run is only valid with --rayjob", + hint="for long-lived clusters, use `nrl-k8s cluster up --role training --dry-run`", + ) + if not submit_mod.is_in_cluster(): _preflight_or_exit(loaded.infra.namespace) @@ -580,14 +502,14 @@ def go( ) _apply_mode_overrides(loaded, submitter=submitter, code_source=code_src, code_path=cli_code_path) click.echo( - f"[go] mode={mode.value} submitter={submitter.value} " + f"[run] mode={mode.value} submitter={submitter.value} " f"code_source={code_src.value} no_wait={no_wait} " f"recreate={recreate} skip_daemons={skip_daemons}", err=True, ) try: - result = orchestrate.go( + result = orchestrate.run( loaded, log=click.echo, repo_root=repo_root.resolve(), @@ -597,99 +519,35 @@ def go( recreate=recreate, ) except Exception as exc: # noqa: BLE001 - _explain_and_exit(exc, context="go failed") + _explain_and_exit(exc, context="run failed") _emit_handle(result.handle) if not no_wait: _follow_handle(result.handle) -@main.command("rayjob") -@click.argument("recipe", type=click.Path(exists=True, dir_okay=False, path_type=Path)) -@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) -@_INFRA_OPTION -@click.option( - "--name", - "name_opt", - type=str, - default=None, - help="RayJob metadata name. Defaults to the training cluster name.", -) -@click.option( - "--shutdown/--no-shutdown", - "shutdown_after", - default=True, - show_default=True, - help="Delete the ephemeral RayCluster once the Ray Job reaches a " - "terminal state (KubeRay's shutdownAfterJobFinishes).", -) -@click.option( - "--ttl", - "ttl_seconds", - type=int, - default=3600, - show_default=True, - help="Seconds to keep the RayJob object after the run finishes " - "(ttlSecondsAfterFinished). Useful for post-mortem log access.", -) -@click.option( - "--wait/--no-wait", - default=True, - help="Poll the RayJob until jobDeploymentStatus is Complete or Failed. " - "Pass --no-wait to return as soon as the RayJob is applied.", -) -@click.option( - "--timeout", - "timeout_s", - type=int, - default=86400, - show_default=True, - help="Seconds to wait for the RayJob to reach a terminal state.", -) -@click.option( - "--dry-run", - is_flag=True, - help="Render the RayJob manifest and print it; do not apply.", -) -def rayjob( +def _run_rayjob( + loaded: LoadedConfig, + *, recipe: Path, - overrides: tuple[str, ...], - infra_path: Path | None, - name_opt: str | None, + name: str | None, shutdown_after: bool, ttl_seconds: int, - wait: bool, timeout_s: int, dry_run: bool, + cli_wait: bool | None, ) -> None: - """Submit the recipe as an ephemeral RayJob. - - KubeRay creates the RayCluster declared under ``clusters.training``, - submits ``infra.launch.entrypoint`` over the dashboard HTTP API, polls - until the driver is terminal, then tears the cluster down. Convenient - for one-shot runs where you don't want a long-lived RayCluster left - over after the job finishes. - - Contrast with ``nrl-k8s run`` / ``launch`` which attach a job to a - long-lived cluster and leave the cluster up for future submissions. - """ + """``nrl-k8s run --rayjob`` path. KubeRay owns the RayCluster lifecycle.""" from . import k8s from . import submit as submit_mod from .rayjob import build_rayjob_manifest - loaded = _load_or_exit(recipe, overrides, infra_path) cluster = _pick_cluster_or_exit(loaded, "training") - if not loaded.infra.launch.entrypoint: - _cli_error( - "infra.launch.entrypoint is empty", - hint="rayjob requires infra.launch.entrypoint; see docs/recipes.md", - ) - manifest = build_rayjob_manifest( cluster, loaded.infra, entrypoint=loaded.infra.launch.entrypoint, - name=name_opt, + name=name, shutdown_after_finishes=shutdown_after, ttl_seconds_after_finished=ttl_seconds, ) @@ -703,7 +561,7 @@ def rayjob( if not submit_mod.is_in_cluster(): _preflight_or_exit(namespace) - click.echo(f"[rayjob] applying RayJob {job_name} in {namespace}") + click.echo(f"[run --rayjob] applying RayJob {job_name} in {namespace}") try: k8s.apply_rayjob(manifest, namespace) except ApiException as exc: @@ -715,13 +573,14 @@ def rayjob( f"follow: kubectl get rayjob {job_name} -n {namespace} -w\n" f"logs: nrl-k8s job logs {recipe} --role training -f", ) - if not wait: + # Default is wait unless user passed --no-wait. + if cli_wait is False: return - click.echo(f"[rayjob] waiting for {job_name} to reach a terminal state ...") + click.echo(f"[run --rayjob] waiting for {job_name} to reach a terminal state ...") def _on_update(deployment: str | None, job: str | None) -> None: - click.echo(f"[rayjob] {job_name} deployment={deployment} job={job}") + click.echo(f"[run --rayjob] {job_name} deployment={deployment} job={job}") try: final = k8s.wait_for_rayjob_terminal( @@ -734,9 +593,9 @@ def _on_update(deployment: str | None, job: str | None) -> None: dep = status.get("jobDeploymentStatus") job_status = status.get("jobStatus") message = (status.get("message") or "").strip() - click.echo(f"[rayjob] {job_name} finished: deployment={dep} job={job_status}") + click.echo(f"[run --rayjob] {job_name} finished: deployment={dep} job={job_status}") if message: - click.echo(f"[rayjob] message: {message}") + click.echo(f"[run --rayjob] message: {message}") sys.exit(0 if dep == "Complete" else 1) diff --git a/tools/nrl_k8s/src/nrl_k8s/orchestrate.py b/tools/nrl_k8s/src/nrl_k8s/orchestrate.py index bad3c0f9c52..2486435a5cb 100644 --- a/tools/nrl_k8s/src/nrl_k8s/orchestrate.py +++ b/tools/nrl_k8s/src/nrl_k8s/orchestrate.py @@ -274,7 +274,7 @@ def submit_training( infra = loaded.infra launch = infra.launch if not launch.entrypoint: - raise ValueError("infra.launch.entrypoint must be set for `nrl-k8s launch`") + raise ValueError("infra.launch.entrypoint must be set for `nrl-k8s run` / `rayjob`") if replace: _reset_endpoint_registry(loaded, log=log) @@ -368,34 +368,20 @@ def run( repo_root: Path, replace: bool = False, run_id: str | None = None, -) -> RunResult: - """Do the full sequence: bring up all 3 clusters + daemons, submit training.""" - if replace: - _reset_endpoint_registry(loaded, log=log) - - for role in ALL_ROLES: - if _get_cluster(loaded.infra, role) is None: - log(f"[{role}] not defined in recipe — skipping") - continue - name = bring_up_cluster(role, loaded, log=log) - submit_daemon(role, loaded, name, log=log, repo_root=repo_root, replace=replace) - - return submit_training( - loaded, log=log, repo_root=repo_root, replace=replace, run_id=run_id - ) - - -def go( - loaded: LoadedConfig, - *, - log: callable, - repo_root: Path, - replace: bool = False, - run_id: str | None = None, skip_daemons: bool = False, recreate: bool = False, ) -> RunResult: - """Idempotent ``run``: reuse matching clusters, warn on drift, then launch.""" + """Idempotent bring-up + daemon + training submit. + + For each declared role, reuse the live RayCluster when its spec matches + the rendered manifest, apply when it is absent, warn + reuse on drift + (or delete + re-apply when ``recreate=True``). Then submit daemons and + the training entrypoint. + + ``skip_daemons=True`` bypasses gym/generation daemon submission — use + when those roles are already healthy and you only want to re-submit + training. + """ if replace: _reset_endpoint_registry(loaded, log=log) @@ -515,7 +501,6 @@ def _wait_for_http(url: str, timeout_s: int, log: callable, role: Role) -> None: "bring_up_cluster", "default_run_id", "ensure_cluster", - "go", "run", "submit_daemon", "submit_training", diff --git a/tools/nrl_k8s/src/nrl_k8s/schema.py b/tools/nrl_k8s/src/nrl_k8s/schema.py index 34b782827c7..4862206d6b9 100644 --- a/tools/nrl_k8s/src/nrl_k8s/schema.py +++ b/tools/nrl_k8s/src/nrl_k8s/schema.py @@ -283,7 +283,7 @@ class LaunchSpec(_StrictModel): attach: AttachSpec = Field(default_factory=AttachSpec) peerWatcher: bool = True # Shell command the training job runs inside the Ray cluster. Required - # for `nrl-k8s launch` / `nrl-k8s run`. Typically a line like + # for `nrl-k8s run` / `nrl-k8s rayjob`. Typically a line like # ``python -u examples/.../entry.py --config nrl_k8s_run.yaml ...``. # The CLI stages the resolved recipe as ``nrl_k8s_run.yaml`` at the # working_dir root so this command can reference it by name (only when diff --git a/tools/nrl_k8s/tests/unit/test_cli.py b/tools/nrl_k8s/tests/unit/test_cli.py index 0cfdb32c3dd..53cc8a78116 100644 --- a/tools/nrl_k8s/tests/unit/test_cli.py +++ b/tools/nrl_k8s/tests/unit/test_cli.py @@ -289,7 +289,7 @@ def test_dry_run_prints_manifest_without_applying(self, tmp_path, monkeypatch): runner = CliRunner() result = runner.invoke( cli.main, - ["rayjob", str(recipe), "--dry-run"], + ["run", str(recipe), "--rayjob", "--dry-run"], ) assert result.exit_code == 0, result.output assert applied == [] @@ -319,7 +319,7 @@ def _fake_wait(name, namespace, *, timeout_s, on_update=None): monkeypatch.setattr("nrl_k8s.submit.is_in_cluster", lambda: True) runner = CliRunner() - result = runner.invoke(cli.main, ["rayjob", str(recipe)]) + result = runner.invoke(cli.main, ["run", str(recipe), "--rayjob"]) assert result.exit_code == 0, result.output assert len(applied) == 1 manifest, ns = applied[0] @@ -343,7 +343,7 @@ def test_failed_job_exits_non_zero(self, tmp_path, monkeypatch): monkeypatch.setattr("nrl_k8s.submit.is_in_cluster", lambda: True) runner = CliRunner() - result = runner.invoke(cli.main, ["rayjob", str(recipe)]) + result = runner.invoke(cli.main, ["run", str(recipe), "--rayjob"]) assert result.exit_code == 1 def test_no_wait_skips_poll(self, tmp_path, monkeypatch): @@ -358,22 +358,22 @@ def test_no_wait_skips_poll(self, tmp_path, monkeypatch): monkeypatch.setattr("nrl_k8s.submit.is_in_cluster", lambda: True) runner = CliRunner() - result = runner.invoke(cli.main, ["rayjob", str(recipe), "--no-wait"]) + result = runner.invoke(cli.main, ["run", str(recipe), "--rayjob", "--no-wait"]) assert result.exit_code == 0, result.output assert waited == [] def test_errors_when_entrypoint_missing(self, tmp_path): recipe = self._recipe_with_training(tmp_path, entrypoint=None) runner = CliRunner() - result = runner.invoke(cli.main, ["rayjob", str(recipe), "--dry-run"]) + result = runner.invoke(cli.main, ["run", str(recipe), "--rayjob", "--dry-run"]) assert result.exit_code == 1 assert "entrypoint" in result.output -class TestGoCommand: - """`nrl-k8s go` delegates to orchestrate.go with the CLI's resolved flags.""" +class TestRunCommand: + """`nrl-k8s run` delegates to orchestrate.run with the CLI's resolved flags.""" - def test_go_invokes_orchestrate_with_flags(self, tmp_path, monkeypatch) -> None: + def test_run_invokes_orchestrate_with_flags(self, tmp_path, monkeypatch) -> None: spec = { "headGroupSpec": { "template": {"spec": {"containers": [{"name": "h", "image": "old"}]}} @@ -404,21 +404,21 @@ class _FakeHandle: class _FakeResult: handle = _FakeHandle() - def _fake_go(loaded, *, log, repo_root, replace, run_id, skip_daemons, recreate): + def _fake_run(loaded, *, log, repo_root, replace, run_id, skip_daemons, recreate): captured["skip_daemons"] = skip_daemons captured["recreate"] = recreate captured["replace"] = replace captured["run_id"] = run_id return _FakeResult() - monkeypatch.setattr("nrl_k8s.orchestrate.go", _fake_go) + monkeypatch.setattr("nrl_k8s.orchestrate.run", _fake_run) monkeypatch.setattr("nrl_k8s.submit.is_in_cluster", lambda: True) runner = CliRunner() result = runner.invoke( cli.main, [ - "go", + "run", str(recipe), "--mode", "batch", "--code-source", "image", diff --git a/tools/nrl_k8s/tests/unit/test_orchestrate.py b/tools/nrl_k8s/tests/unit/test_orchestrate.py index 26cb809b844..99bde3c01e0 100644 --- a/tools/nrl_k8s/tests/unit/test_orchestrate.py +++ b/tools/nrl_k8s/tests/unit/test_orchestrate.py @@ -359,11 +359,11 @@ def test_recreate_deletes_and_reapplies_on_drift(self, monkeypatch, log) -> None # ============================================================================= -# go — idempotent run +# run — idempotent bring-up + daemon + training submit # ============================================================================= -class TestGo: +class TestRun: def test_skip_daemons_bypasses_daemon_submit(self, monkeypatch, log) -> None: log_fn, lines = log loaded = _loaded(gym_entrypoint="python gym.py --job-id run-q") @@ -375,7 +375,7 @@ def test_skip_daemons_bypasses_daemon_submit(self, monkeypatch, log) -> None: monkeypatch.setattr(orchestrate, "submit_daemon", daemon) monkeypatch.setattr(orchestrate, "submit_training", train) - out = orchestrate.go( + out = orchestrate.run( loaded, log=log_fn, repo_root=Path("/tmp"), skip_daemons=True ) @@ -397,7 +397,7 @@ def test_recreate_flag_propagates(self, monkeypatch, log) -> None: monkeypatch.setattr(orchestrate, "submit_daemon", MagicMock()) monkeypatch.setattr(orchestrate, "submit_training", MagicMock()) - orchestrate.go( + orchestrate.run( loaded, log=log_fn, repo_root=Path("/tmp"), recreate=True ) From 4070d9459489e2799cc9d91aa8befaf4d1b1ed1c Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Wed, 22 Apr 2026 17:58:21 -0700 Subject: [PATCH 36/84] docs(nrl-k8s): reference k8s skill from CLAUDE.md Point CLAUDE.md (AGENTS.md) at the launch-nemo-rl skill so it is discoverable from any subdirectory in the repo. Signed-off-by: Terry Kong --- AGENTS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 023083c4413..216f58abfbe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,3 +17,7 @@ When reviewing code, follow these principles: - **High confidence only.** Only flag issues you are confident about. If unsure, skip it. - **Verify upstream API usage.** When code calls into megatron-bridge, megatron-lm, automodel, or gym APIs, look up the actual API to verify correct usage. Evaluate each such call with scrutiny — don't assume the author got the signature, return type, or semantics right. - It is perfectly acceptable to have nothing to comment on. Say "LGTM" if so. + +## Kubernetes / nrl-k8s + +For launching, monitoring, stopping, and debugging NeMo-RL recipes on Kubernetes, see the skill at `tools/nrl_k8s/.claude/skills/launch-nemo-rl/SKILL.md`. From a1c028d8eb449f1c6f57497002c5db39cb4be8de Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Wed, 22 Apr 2026 18:05:38 -0700 Subject: [PATCH 37/84] fix up skill pointer Signed-off-by: Terry Kong --- AGENTS.md | 4 ++-- .../nrl_k8s/.claude/skills => skills}/launch-nemo-rl/SKILL.md | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename {tools/nrl_k8s/.claude/skills => skills}/launch-nemo-rl/SKILL.md (100%) diff --git a/AGENTS.md b/AGENTS.md index 216f58abfbe..97e1365a697 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ NeMo-RL is an RLHF training framework built on Ray and PyTorch (FSDP2 / Megatron ## Coding Guidelines -Coding guidelines are organized as Claude skills in `.claude/skills/`. Each skill covers a specific topic (style, config conventions, error handling, testing, copyright, docs). +Coding guidelines are organized as Claude skills in `skills/`. Each skill covers a specific topic (style, config conventions, error handling, testing, copyright, docs). ## Code Review @@ -20,4 +20,4 @@ When reviewing code, follow these principles: ## Kubernetes / nrl-k8s -For launching, monitoring, stopping, and debugging NeMo-RL recipes on Kubernetes, see the skill at `tools/nrl_k8s/.claude/skills/launch-nemo-rl/SKILL.md`. +For launching, monitoring, stopping, and debugging NeMo-RL recipes on Kubernetes, see the skill at `skills/launch-nemo-rl/SKILL.md`. diff --git a/tools/nrl_k8s/.claude/skills/launch-nemo-rl/SKILL.md b/skills/launch-nemo-rl/SKILL.md similarity index 100% rename from tools/nrl_k8s/.claude/skills/launch-nemo-rl/SKILL.md rename to skills/launch-nemo-rl/SKILL.md From 5034a224b5e6935add41fd93bfbd4c2ce5658dee Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Wed, 22 Apr 2026 18:44:22 -0700 Subject: [PATCH 38/84] infra: add 64-node monolithic RayJob + KAI topology + move README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add rayjob-monolithic-64n.yaml: 4×16 segment topology-constrained RayJob for GB300 (256 GPUs) - Add kai-topology.yaml: two-level clique→hostname topology for KAI - Move kind/SETUP.md → README.md, fix relative paths Signed-off-by: Terry Kong --- infra/{kind/SETUP.md => README.md} | 6 +- infra/examples/kai-topology.yaml | 16 + infra/examples/rayjob-monolithic-64n.yaml | 417 ++++++++++++++++++++++ 3 files changed, 436 insertions(+), 3 deletions(-) rename infra/{kind/SETUP.md => README.md} (99%) create mode 100644 infra/examples/kai-topology.yaml create mode 100644 infra/examples/rayjob-monolithic-64n.yaml diff --git a/infra/kind/SETUP.md b/infra/README.md similarity index 99% rename from infra/kind/SETUP.md rename to infra/README.md index f5ec037e7d4..3f339ac2b8f 100644 --- a/infra/kind/SETUP.md +++ b/infra/README.md @@ -35,7 +35,7 @@ stat -fc %T /sys/fs/cgroup/ # should show "cgroup2fs" ```sh # 1. Install tools -cd infra/kind +cd kind bash install-nvkind.sh bash get-kubectl.sh bash get-helm.sh @@ -76,9 +76,9 @@ python examples/nemo_gym/run_grpo_nemo_gym.py +env.disagg_job_id=my-job logger.w ## Deploy on a real cluster ```sh -cd infra/helm +cd helm helmfile -e prod sync -kubectl apply -f infra/examples/kai-queue-prod.yaml +kubectl apply -f examples/kai-queue-prod.yaml ``` This installs KAI scheduler, KubeRay, and JobSet. The cluster is expected to already have the GPU Operator (or equivalent GPU provisioning) installed. diff --git a/infra/examples/kai-topology.yaml b/infra/examples/kai-topology.yaml new file mode 100644 index 00000000000..0a1e94dea38 --- /dev/null +++ b/infra/examples/kai-topology.yaml @@ -0,0 +1,16 @@ +# KAI Topology for GB300 clique-aware scheduling. +# +# Defines a two-level hierarchy: clique → hostname. KAI uses this to +# constrain worker groups annotated with +# kai.scheduler/topology: gb300-topology +# kai.scheduler/topology-required-placement: nvidia.com/gpu.clique +# so that all pods in a group land on nodes within the same clique. + +apiVersion: kai.scheduler/v1alpha1 +kind: Topology +metadata: + name: gb300-topology +spec: + levels: + - nodeLabel: nvidia.com/gpu.clique + - nodeLabel: kubernetes.io/hostname diff --git a/infra/examples/rayjob-monolithic-64n.yaml b/infra/examples/rayjob-monolithic-64n.yaml new file mode 100644 index 00000000000..94ead24f6ba --- /dev/null +++ b/infra/examples/rayjob-monolithic-64n.yaml @@ -0,0 +1,417 @@ +# Monolithic RayJob for GB300 — 64 GPU nodes (256 GPUs), segment-size 16. +# +# KubeRay creates a RayCluster, waits for all pods to be Ready, submits the +# entrypoint via HTTP, polls until completion, then tears down the cluster. +# +# Structure: +# head — CPU-only Ray head on a customer-cpu node (GCS + dashboard) +# segment-0..3 — 4 worker groups of 16 GPU pods each, topology-constrained +# +# Topology: each worker group has kai.scheduler/topology-required-placement +# annotations (KAI PR #1125) to place all 16 pods in the same clique. +# +# Prerequisites: +# kubectl apply -f kai-queue.yaml +# kubectl apply -f kai-topology.yaml +# +# Usage: +# kubectl apply -f rayjob-monolithic-64n.yaml +# kubectl get rayjob sft-job-64n -w +# +# Logs: +# kubectl logs -f -l ray.io/cluster=sft-job-64n-raycluster --prefix # all pods +# kubectl exec -it -- ray job logs --follow # via Ray +# kubectl exec -it -- cat /tmp/ray/session_latest/logs/... # Ray file logs +# +# Cleanup: +# kubectl delete rayjob sft-job-64n +# kubectl delete computedomain compute-domain-rayjob-64n +# kubectl delete resourceclaimtemplate roce-rayjob-64n + +# --- 1. ComputeDomain (controller auto-creates the ResourceClaimTemplate) --- +apiVersion: resource.nvidia.com/v1beta1 +kind: ComputeDomain +metadata: + name: compute-domain-rayjob-64n +spec: + channel: + resourceClaimTemplate: + name: compute-domain-rayjob-64n + numNodes: 0 +--- +# --- 2. RoCE ResourceClaimTemplate --- +apiVersion: resource.k8s.io/v1 +kind: ResourceClaimTemplate +metadata: + name: roce-rayjob-64n +spec: + spec: + devices: + requests: + - exactly: + count: 8 + deviceClassName: roce.networking.k8s.aws + name: roce +--- +# --- RayJob --- +apiVersion: ray.io/v1 +kind: RayJob +metadata: + name: sft-job-64n + labels: + kai.scheduler/queue: default-queue +spec: + entrypoint: | + LOG=/mnt/rl-workspace/logs/sft-64n-$(date +%Y%m%d-%H%M%S).log + mkdir -p /mnt/rl-workspace/logs + cd /opt/nemo-rl + python -u examples/run_sft.py \ + --config examples/configs/sft.yaml \ + policy.model_name=Qwen/Qwen3-0.6B \ + cluster.gpus_per_node=4 \ + cluster.num_nodes=64 \ + policy.train_global_batch_size=512 \ + policy.train_micro_batch_size=1 \ + policy.max_total_sequence_length=1024 \ + policy.dtensor_cfg.tensor_parallel_size=1 \ + sft.max_num_steps=10 \ + sft.val_period=5 \ + sft.val_global_batch_size=512 \ + checkpointing.enabled=false \ + logger.wandb_enabled=true \ + logger.wandb.project=nemorl-rayjob-64n \ + logger.wandb.name=sft-64n-gb300 \ + logger.tensorboard_enabled=false \ + 2>&1 | tee "$LOG" + # HTTPMode submits via the Ray dashboard API. K8sJobMode creates a separate + # K8s Job which breaks KAI gang scheduling. + submissionMode: HTTPMode + shutdownAfterJobFinishes: true + ttlSecondsAfterFinished: 60 + rayClusterSpec: + rayVersion: "2.52.0" + # Disable KubeRay's autoscaler sidecar — we use fixed-size worker groups. + enableInTreeAutoscaling: false + + # ======================== + # Ray head (CPU-only, on customer-cpu node) + # ======================== + headGroupSpec: + rayStartParams: + num-gpus: "0" + object-store-memory: "200000000" + # Disable the Ray monitor/autoscaler process. Without this, ray start + # --head launches a monitor that removes "excess" workers even when + # enableInTreeAutoscaling is false. The monitor sees more workers than + # its internal max and continuously removes them, starving the job of GPUs. + no-monitor: "true" + template: + spec: + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + # Schedule head on CPU-only nodes (m8g.16xlarge, 64 vCPU / 250 GiB). + # The head runs GCS + dashboard for 256 GPU actors, spawning ~500 Python + # processes — needs generous CPU/memory but no GPUs. + nodeSelector: + nodeGroup: customer-cpu + containers: + - name: ray-head + image: nvcr.io/nvidian/nemo-rl:nightly + env: + - { name: RAY_memory_monitor_refresh_ms, value: "0" } + - name: HF_TOKEN + valueFrom: + secretKeyRef: { name: terryk-secrets, key: HF_TOKEN } + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: { name: terryk-secrets, key: WANDB_API_KEY } + - { name: HF_HOME, value: /mnt/rl-workspace/shared/hf-cache } + - { name: HF_DATASETS_CACHE, value: /mnt/rl-workspace/shared/hf-cache/datasets } + - { name: TRANSFORMERS_CACHE, value: /mnt/rl-workspace/shared/hf-cache } + resources: + requests: { cpu: "48", memory: "200Gi" } + limits: { cpu: "60", memory: "240Gi" } + ports: + - { containerPort: 6379, name: gcs-server } + - { containerPort: 8265, name: dashboard } + - { containerPort: 10001, name: client } + readinessProbe: + exec: + command: ["ray", "health-check"] + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 10 + failureThreshold: 30 + volumeMounts: + - { name: dshm, mountPath: /dev/shm } + - { name: rl-workspace, mountPath: /mnt/rl-workspace } + volumes: + - name: dshm + emptyDir: { medium: Memory, sizeLimit: 32Gi } + - name: rl-workspace + persistentVolumeClaim: { claimName: rl-workspace } + + # ======================== + # GPU worker segments (4 × 16 = 64 nodes, 256 GPUs) + # + # Split into 4 worker groups instead of 1 group of 64 so that KAI's + # topology-aware scheduling (PR #1125) places each group's 16 pods + # in the same clique. KubeRay doesn't support kai.scheduler/segment-size, + # so manual group splitting is required. + # + # Each group has topology annotations that KAI's Ray PodGrouper reads + # and propagates to PodGroup subgroups (unlike JobSets, this works). + # ======================== + workerGroupSpecs: + - groupName: segment-0 + replicas: 16 + minReplicas: 16 + maxReplicas: 16 + rayStartParams: + num-gpus: "4" + object-store-memory: "200000000" + template: + metadata: + annotations: + kai.scheduler/topology: gb300-topology + kai.scheduler/topology-required-placement: nvidia.com/gpu.clique + spec: + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + tolerations: + - operator: Exists + containers: + - name: ray-worker + image: nvcr.io/nvidian/nemo-rl:nightly + env: + - { name: RAY_memory_monitor_refresh_ms, value: "0" } + - name: HF_TOKEN + valueFrom: + secretKeyRef: { name: terryk-secrets, key: HF_TOKEN } + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: { name: terryk-secrets, key: WANDB_API_KEY } + - { name: HF_HOME, value: /mnt/rl-workspace/shared/hf-cache } + - { name: HF_DATASETS_CACHE, value: /mnt/rl-workspace/shared/hf-cache/datasets } + - { name: TRANSFORMERS_CACHE, value: /mnt/rl-workspace/shared/hf-cache } + - { name: NCCL_DEBUG, value: WARN } + - { name: NCCL_MNNVL_ENABLE, value: "1" } + resources: + claims: + - name: compute-domain-channel + - name: roce-channel + requests: + cpu: "120" + memory: "850Gi" + nvidia.com/gpu: "4" + limits: + cpu: "132" + memory: "900Gi" + nvidia.com/gpu: "4" + volumeMounts: + - { name: dshm, mountPath: /dev/shm } + - { name: rl-workspace, mountPath: /mnt/rl-workspace } + securityContext: + capabilities: + add: [IPC_LOCK] + resourceClaims: + - name: compute-domain-channel + resourceClaimTemplateName: compute-domain-rayjob-64n + - name: roce-channel + resourceClaimTemplateName: roce-rayjob-64n + volumes: + - name: dshm + emptyDir: { medium: Memory, sizeLimit: 64Gi } + - name: rl-workspace + persistentVolumeClaim: { claimName: rl-workspace } + + - groupName: segment-1 + replicas: 16 + minReplicas: 16 + maxReplicas: 16 + rayStartParams: + num-gpus: "4" + object-store-memory: "200000000" + template: + metadata: + annotations: + kai.scheduler/topology: gb300-topology + kai.scheduler/topology-required-placement: nvidia.com/gpu.clique + spec: + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + tolerations: + - operator: Exists + containers: + - name: ray-worker + image: nvcr.io/nvidian/nemo-rl:nightly + env: + - { name: RAY_memory_monitor_refresh_ms, value: "0" } + - name: HF_TOKEN + valueFrom: + secretKeyRef: { name: terryk-secrets, key: HF_TOKEN } + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: { name: terryk-secrets, key: WANDB_API_KEY } + - { name: HF_HOME, value: /mnt/rl-workspace/shared/hf-cache } + - { name: HF_DATASETS_CACHE, value: /mnt/rl-workspace/shared/hf-cache/datasets } + - { name: TRANSFORMERS_CACHE, value: /mnt/rl-workspace/shared/hf-cache } + - { name: NCCL_DEBUG, value: WARN } + - { name: NCCL_MNNVL_ENABLE, value: "1" } + resources: + claims: + - name: compute-domain-channel + - name: roce-channel + requests: + cpu: "120" + memory: "850Gi" + nvidia.com/gpu: "4" + limits: + cpu: "132" + memory: "900Gi" + nvidia.com/gpu: "4" + volumeMounts: + - { name: dshm, mountPath: /dev/shm } + - { name: rl-workspace, mountPath: /mnt/rl-workspace } + securityContext: + capabilities: + add: [IPC_LOCK] + resourceClaims: + - name: compute-domain-channel + resourceClaimTemplateName: compute-domain-rayjob-64n + - name: roce-channel + resourceClaimTemplateName: roce-rayjob-64n + volumes: + - name: dshm + emptyDir: { medium: Memory, sizeLimit: 64Gi } + - name: rl-workspace + persistentVolumeClaim: { claimName: rl-workspace } + + - groupName: segment-2 + replicas: 16 + minReplicas: 16 + maxReplicas: 16 + rayStartParams: + num-gpus: "4" + object-store-memory: "200000000" + template: + metadata: + annotations: + kai.scheduler/topology: gb300-topology + kai.scheduler/topology-required-placement: nvidia.com/gpu.clique + spec: + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + tolerations: + - operator: Exists + containers: + - name: ray-worker + image: nvcr.io/nvidian/nemo-rl:nightly + env: + - { name: RAY_memory_monitor_refresh_ms, value: "0" } + - name: HF_TOKEN + valueFrom: + secretKeyRef: { name: terryk-secrets, key: HF_TOKEN } + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: { name: terryk-secrets, key: WANDB_API_KEY } + - { name: HF_HOME, value: /mnt/rl-workspace/shared/hf-cache } + - { name: HF_DATASETS_CACHE, value: /mnt/rl-workspace/shared/hf-cache/datasets } + - { name: TRANSFORMERS_CACHE, value: /mnt/rl-workspace/shared/hf-cache } + - { name: NCCL_DEBUG, value: WARN } + - { name: NCCL_MNNVL_ENABLE, value: "1" } + resources: + claims: + - name: compute-domain-channel + - name: roce-channel + requests: + cpu: "120" + memory: "850Gi" + nvidia.com/gpu: "4" + limits: + cpu: "132" + memory: "900Gi" + nvidia.com/gpu: "4" + volumeMounts: + - { name: dshm, mountPath: /dev/shm } + - { name: rl-workspace, mountPath: /mnt/rl-workspace } + securityContext: + capabilities: + add: [IPC_LOCK] + resourceClaims: + - name: compute-domain-channel + resourceClaimTemplateName: compute-domain-rayjob-64n + - name: roce-channel + resourceClaimTemplateName: roce-rayjob-64n + volumes: + - name: dshm + emptyDir: { medium: Memory, sizeLimit: 64Gi } + - name: rl-workspace + persistentVolumeClaim: { claimName: rl-workspace } + + - groupName: segment-3 + replicas: 16 + minReplicas: 16 + maxReplicas: 16 + rayStartParams: + num-gpus: "4" + object-store-memory: "200000000" + template: + metadata: + annotations: + kai.scheduler/topology: gb300-topology + kai.scheduler/topology-required-placement: nvidia.com/gpu.clique + spec: + schedulerName: kai-scheduler + imagePullSecrets: + - name: nvcr-secret + tolerations: + - operator: Exists + containers: + - name: ray-worker + image: nvcr.io/nvidian/nemo-rl:nightly + env: + - { name: RAY_memory_monitor_refresh_ms, value: "0" } + - name: HF_TOKEN + valueFrom: + secretKeyRef: { name: terryk-secrets, key: HF_TOKEN } + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: { name: terryk-secrets, key: WANDB_API_KEY } + - { name: HF_HOME, value: /mnt/rl-workspace/shared/hf-cache } + - { name: HF_DATASETS_CACHE, value: /mnt/rl-workspace/shared/hf-cache/datasets } + - { name: TRANSFORMERS_CACHE, value: /mnt/rl-workspace/shared/hf-cache } + - { name: NCCL_DEBUG, value: WARN } + - { name: NCCL_MNNVL_ENABLE, value: "1" } + resources: + claims: + - name: compute-domain-channel + - name: roce-channel + requests: + cpu: "120" + memory: "850Gi" + nvidia.com/gpu: "4" + limits: + cpu: "132" + memory: "900Gi" + nvidia.com/gpu: "4" + volumeMounts: + - { name: dshm, mountPath: /dev/shm } + - { name: rl-workspace, mountPath: /mnt/rl-workspace } + securityContext: + capabilities: + add: [IPC_LOCK] + resourceClaims: + - name: compute-domain-channel + resourceClaimTemplateName: compute-domain-rayjob-64n + - name: roce-channel + resourceClaimTemplateName: roce-rayjob-64n + volumes: + - name: dshm + emptyDir: { medium: Memory, sizeLimit: 64Gi } + - name: rl-workspace + persistentVolumeClaim: { claimName: rl-workspace } From 7ffa25d3f4f5b99808210540e24a23d06697bff9 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Wed, 22 Apr 2026 18:47:15 -0700 Subject: [PATCH 39/84] docs(nrl-k8s): add active-development warning banner Signed-off-by: Terry Kong --- tools/nrl_k8s/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/nrl_k8s/README.md b/tools/nrl_k8s/README.md index d334ab3ea77..d200142ebc9 100644 --- a/tools/nrl_k8s/README.md +++ b/tools/nrl_k8s/README.md @@ -1,5 +1,9 @@ # nrl-k8s +> [!WARNING] +> These instructions are in active development and should not be relied on as stable yet. +> APIs, manifests, and tooling may change without notice. + A config-driven launcher that runs a NeMo-RL recipe on any Kubernetes cluster with the KubeRay operator installed. One YAML pair captures *what to train* (the recipe) and *where to run it* (the infra); the CLI brings up every From 8d615d724a0375369e392ae5b37c60fa340fe606 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Wed, 22 Apr 2026 19:57:04 -0700 Subject: [PATCH 40/84] feat(nrl-k8s): add ${user:} resolver for per-user cluster names Register a ${user:} OmegaConf resolver (getpass.getuser(), sanitised for K8s names, overridable via NRL_K8S_USER). All example infra YAMLs now prefix cluster names with ${user:} and set nrl-k8s/owner: ${user:} as a top-level label. ensure_cluster() checks the owner label on live clusters and refuses to touch resources owned by a different user. Signed-off-by: Terry Kong --- .../qwen3_30b_math_8n_4gpu.gb300.infra.yaml | 7 +++++-- .../examples/qwen3_4b_if_full_disagg.infra.yaml | 15 +++++++++------ .../examples/qwen3_4b_if_gym_disagg.infra.yaml | 11 +++++++---- .../qwen3_4b_if_gym_disagg.prod.infra.yaml | 11 +++++++---- .../examples/qwen3_4b_if_single.gb300.infra.yaml | 7 +++++-- .../qwen3_4b_if_single.gb300.prod.infra.yaml | 7 +++++-- .../examples/qwen3_4b_if_single.infra.yaml | 7 +++++-- tools/nrl_k8s/src/nrl_k8s/config.py | 13 +++++++++++++ tools/nrl_k8s/src/nrl_k8s/orchestrate.py | 13 ++++++++++++- 9 files changed, 68 insertions(+), 23 deletions(-) diff --git a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml index ebb201511b6..440800a612b 100644 --- a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml @@ -105,6 +105,9 @@ image: nvcr.io/nvidian/nemo-rl:nightly imagePullSecrets: [nvcr-secret] serviceAccount: nemo-rl-endpoint-registry +labels: + nrl-k8s/owner: ${user:} + submit: submitter: portForward @@ -114,7 +117,7 @@ launch: codeSource: image codePath: /opt/nemo-rl attach: - training: raycluster-qwen3-30b-math-gb300 + training: ${user:}-raycluster-qwen3-30b-math-gb300 peerWatcher: false env: {} # Runs inside the training head pod under Ray's Job SDK. Megatron's optimizer @@ -142,7 +145,7 @@ launch: clusters: training: - name: raycluster-qwen3-30b-math-gb300 + name: ${user:}-raycluster-qwen3-30b-math-gb300 labels: disagg.nemo-rl/cluster: qwen3-30b-math-gb300 disagg.nemo-rl/run: qwen3-30b-math-gb300 diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml index de29688e6b2..c9dd942b71f 100644 --- a/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml @@ -84,12 +84,15 @@ image: nvcr.io/nvidian/nemo-rl:nightly imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] serviceAccount: nemo-rl-endpoint-registry +labels: + nrl-k8s/owner: ${user:} + launch: mode: attach attach: - generation: raycluster-generation-qwen3-4b - gym: raycluster-gym-qwen3-4b - training: raycluster-rl-qwen3-4b + generation: ${user:}-raycluster-generation-qwen3-4b + gym: ${user:}-raycluster-gym-qwen3-4b + training: ${user:}-raycluster-rl-qwen3-4b peerWatcher: false # Minimal upload set — Ray caps working_dir at 100 MiB. We include only # what this run needs: nemo_rl, examples, the nemo_gym subset, and the @@ -165,7 +168,7 @@ clusters: # Generation — GPU head + worker, control-server on 8089. # ------------------------------------------------------------------------- generation: - name: raycluster-generation-qwen3-4b + name: ${user:}-raycluster-generation-qwen3-4b labels: disagg.nemo-rl/cluster: generation-qwen3-4b disagg.nemo-rl/run: qwen3-4b-gym @@ -247,7 +250,7 @@ clusters: # Gym — CPU-only head, gym-server on 9090. # ------------------------------------------------------------------------- gym: - name: raycluster-gym-qwen3-4b + name: ${user:}-raycluster-gym-qwen3-4b daemon: submissionId: qwen3-4b-if-gym-server-v7 # Ray runs entrypoints under /bin/dash. We stay POSIX here (no @@ -324,7 +327,7 @@ clusters: # Training — GPU head + worker. Head has WANDB secret. # ------------------------------------------------------------------------- training: - name: raycluster-rl-qwen3-4b + name: ${user:}-raycluster-rl-qwen3-4b labels: disagg.nemo-rl/cluster: rl-qwen3-4b disagg.nemo-rl/run: qwen3-4b-gym diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml index 789e9d9ac7e..3794350e956 100644 --- a/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml @@ -60,11 +60,14 @@ image: nvcr.io/nvidian/nemo-rl:nightly imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] serviceAccount: nemo-rl-endpoint-registry +labels: + nrl-k8s/owner: ${user:} + launch: mode: attach attach: - gym: raycluster-gym-disagg-gym-qwen3-4b - training: raycluster-gym-disagg-qwen3-4b + gym: ${user:}-raycluster-gym-disagg-gym-qwen3-4b + training: ${user:}-raycluster-gym-disagg-qwen3-4b peerWatcher: false # Colocated generation → no --remote_generation_url override. # All env flows through inline exports (runtime_env.env_vars merge-fails @@ -106,7 +109,7 @@ launch: clusters: # ---------------- Gym (CPU, head-only) ---------------- gym: - name: raycluster-gym-disagg-gym-qwen3-4b + name: ${user:}-raycluster-gym-disagg-gym-qwen3-4b daemon: submissionId: qwen3-4b-gym-disagg-server-v5 entrypoint: | @@ -168,7 +171,7 @@ clusters: # ---------------- Training (GPU head + worker, generation colocated) ---------------- training: - name: raycluster-gym-disagg-qwen3-4b + name: ${user:}-raycluster-gym-disagg-qwen3-4b labels: disagg.nemo-rl/cluster: gym-disagg-qwen3-4b disagg.nemo-rl/run: qwen3-4b-gym-disagg diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml index 8dd284654f1..86adb1b23a9 100644 --- a/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml @@ -80,6 +80,9 @@ image: nvcr.io/nvidian/nemo-rl:nightly imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] serviceAccount: nemo-rl-endpoint-registry +labels: + nrl-k8s/owner: ${user:} + submit: # Shell into the training head pod instead of port-forwarding the # dashboard to the laptop. Once `nohup` + `disown` fire the laptop is @@ -97,8 +100,8 @@ launch: codeSource: image codePath: /opt/nemo-rl attach: - gym: raycluster-gym-disagg-gym-qwen3-4b - training: raycluster-gym-disagg-qwen3-4b + gym: ${user:}-raycluster-gym-disagg-gym-qwen3-4b + training: ${user:}-raycluster-gym-disagg-qwen3-4b peerWatcher: false env: {} # Entrypoint runs inside the training head pod under `nohup bash`. @@ -131,7 +134,7 @@ clusters: # is paid once per `cluster up` and the researcher's `launch` never # touches it. gym: - name: raycluster-gym-disagg-gym-qwen3-4b + name: ${user:}-raycluster-gym-disagg-gym-qwen3-4b daemon: submissionId: qwen3-4b-gym-disagg-server-v5 entrypoint: | @@ -190,7 +193,7 @@ clusters: # ---------------- Training (GPU head + worker, generation colocated) ---------------- training: - name: raycluster-gym-disagg-qwen3-4b + name: ${user:}-raycluster-gym-disagg-qwen3-4b labels: disagg.nemo-rl/cluster: gym-disagg-qwen3-4b disagg.nemo-rl/run: qwen3-4b-gym-disagg diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml index 62724afc95c..7f23fabe063 100644 --- a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml @@ -55,10 +55,13 @@ image: nvcr.io/nvidian/nemo-rl:nightly imagePullSecrets: [nvcr-secret] serviceAccount: nemo-rl-endpoint-registry +labels: + nrl-k8s/owner: ${user:} + launch: mode: attach attach: - training: raycluster-single-qwen3-4b-gb300 + training: ${user:}-raycluster-single-qwen3-4b-gb300 peerWatcher: false entrypoint: | pip install kubernetes -q || true @@ -98,7 +101,7 @@ launch: clusters: # ---------------- Training (GPU head + worker: generation + gym colocated) ---------------- training: - name: raycluster-single-qwen3-4b-gb300 + name: ${user:}-raycluster-single-qwen3-4b-gb300 labels: disagg.nemo-rl/cluster: single-qwen3-4b-gb300 disagg.nemo-rl/run: qwen3-4b-single-gb300 diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml index ae3e3f8610c..b716dd62b16 100644 --- a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml @@ -85,6 +85,9 @@ image: nvcr.io/nvidian/nemo-rl:nightly imagePullSecrets: [nvcr-secret] serviceAccount: nemo-rl-endpoint-registry +labels: + nrl-k8s/owner: ${user:} + submit: submitter: portForward @@ -94,7 +97,7 @@ launch: codeSource: image codePath: /opt/nemo-rl attach: - training: raycluster-single-qwen3-4b-gb300-prod + training: ${user:}-raycluster-single-qwen3-4b-gb300-prod peerWatcher: false env: {} # Runs inside the training head pod under Ray's Job SDK. cwd inherits @@ -121,7 +124,7 @@ launch: clusters: training: - name: raycluster-single-qwen3-4b-gb300-prod + name: ${user:}-raycluster-single-qwen3-4b-gb300-prod labels: disagg.nemo-rl/cluster: single-qwen3-4b-gb300-prod disagg.nemo-rl/run: qwen3-4b-single-gb300-prod diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml index 9f0ee75977d..de51edf3722 100644 --- a/tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml @@ -67,10 +67,13 @@ image: nvcr.io/nvidian/nemo-rl:nightly imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] serviceAccount: nemo-rl-endpoint-registry +labels: + nrl-k8s/owner: ${user:} + launch: mode: attach attach: - training: raycluster-single-qwen3-4b + training: ${user:}-raycluster-single-qwen3-4b peerWatcher: false # Single-cluster: no remote_generation_url (colocated), no disagg_job_id # (no endpoint registry — gym is a local Ray actor). @@ -115,7 +118,7 @@ launch: clusters: # ---------------- Training (GPU head + worker: generation + gym colocated) ---------------- training: - name: raycluster-single-qwen3-4b + name: ${user:}-raycluster-single-qwen3-4b labels: disagg.nemo-rl/cluster: single-qwen3-4b disagg.nemo-rl/run: qwen3-4b-single diff --git a/tools/nrl_k8s/src/nrl_k8s/config.py b/tools/nrl_k8s/src/nrl_k8s/config.py index 93e78b6d97a..9a6b998596d 100644 --- a/tools/nrl_k8s/src/nrl_k8s/config.py +++ b/tools/nrl_k8s/src/nrl_k8s/config.py @@ -23,6 +23,7 @@ from __future__ import annotations +import getpass import os from dataclasses import dataclass from pathlib import Path @@ -31,6 +32,17 @@ from .schema import InfraConfig + +def get_username() -> str: + """Return the local OS username, sanitised for K8s resource names.""" + raw = os.environ.get("NRL_K8S_USER") or getpass.getuser() + return raw.lower().replace("_", "-").replace(".", "-") + + +def _register_nrl_resolvers() -> None: + if not OmegaConf.has_resolver("user"): + OmegaConf.register_new_resolver("user", lambda: get_username()) + _SHIPPED_DEFAULTS = Path(__file__).parent / "defaults" / "defaults.example.yaml" _USER_DEFAULTS = Path( os.environ.get( @@ -83,6 +95,7 @@ def load_recipe_with_infra( """ overrides = overrides or [] recipe_path = Path(recipe_path).resolve() + _register_nrl_resolvers() recipe_overrides, infra_overrides = _partition_overrides(overrides) recipe = _load_recipe(recipe_path, overrides=recipe_overrides) diff --git a/tools/nrl_k8s/src/nrl_k8s/orchestrate.py b/tools/nrl_k8s/src/nrl_k8s/orchestrate.py index 2486435a5cb..45a7567ecbb 100644 --- a/tools/nrl_k8s/src/nrl_k8s/orchestrate.py +++ b/tools/nrl_k8s/src/nrl_k8s/orchestrate.py @@ -31,7 +31,7 @@ from ray.job_submission import JobStatus, JobSubmissionClient from . import k8s, submit, workdir -from .config import LoadedConfig +from .config import LoadedConfig, get_username from .manifest import build_raycluster_manifest from .schema import ClusterSpec, CodeSource, InfraConfig, SubmitterMode from .submitters import SubmissionHandle, build_submitter, save_handle @@ -111,6 +111,17 @@ def ensure_cluster( namespace = loaded.infra.namespace live = k8s.get_raycluster(name, namespace) + if live is not None: + live_owner = (live.get("metadata", {}).get("labels") or {}).get( + "nrl-k8s/owner" + ) + me = get_username() + if live_owner and live_owner != me: + raise RuntimeError( + f"RayCluster {name} in namespace {namespace} is owned by " + f"'{live_owner}' (you are '{me}'). Use a different cluster " + f"name or ask {live_owner} to tear it down." + ) if live is None: log(f"[{role}] applying RayCluster {name} in namespace {namespace}") k8s.apply_raycluster(manifest, namespace) From d63b80f34d01b4989c0449bdbdc609ad560bedd2 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Wed, 22 Apr 2026 22:35:18 -0700 Subject: [PATCH 41/84] feat(nrl-k8s): parametrize hemild paths with user resolver in gb300 infra Signed-off-by: Terry Kong --- .../examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml | 12 ++++++------ .../qwen3_4b_if_single.gb300.prod.infra.yaml | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml index 440800a612b..1b09cc773c6 100644 --- a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml @@ -15,12 +15,12 @@ # launch.runMode batch — CLI returns after Ray accepts the job. # launch.codeSource image — no working_dir upload. # launch.codePath /opt/nemo-rl — RL repo served off the shared FSx -# Lustre PVC at subPath=hemild/rl-k8s. +# Lustre PVC at subPath=${user:}/rl-k8s. # Edits on Lustre take effect without rebuilding the image. # # Prereqs (onboarding §3–§5 already applied in `default`): # - rl-workspace PVC bound (shared FSx Lustre, 2.4 TiB, RWX). -# - /mnt/rl-workspace/hemild/rl-k8s contains a git checkout of +# - /mnt/rl-workspace/${user:}/rl-k8s contains a git checkout of # https://github.com/NVIDIA-NeMo/RL on branch hemil/k8s-infra-cp. # - nvcr-secret, wandb-api-key, nemo-rl-endpoint-registry all present. # - ResourceClaimTemplates compute-domain-qwen3-30b-math and @@ -45,7 +45,7 @@ _shared: workerNodeSelector: &worker_node_selector nvidia.com/gpu.product: NVIDIA-GB300 headEnv: &shared_head_env - - {name: HF_HOME, value: /mnt/rl-workspace/hemild/hf-cache} + - {name: HF_HOME, value: "/mnt/rl-workspace/${user:}/hf-cache"} - {name: HF_HUB_OFFLINE, value: "1"} - {name: TRANSFORMERS_OFFLINE, value: "1"} - {name: NCCL_DEBUG, value: WARN} @@ -56,7 +56,7 @@ _shared: name: wandb-api-key key: WANDB_API_KEY workerEnv: &shared_worker_env - - {name: HF_HOME, value: /mnt/rl-workspace/hemild/hf-cache} + - {name: HF_HOME, value: "/mnt/rl-workspace/${user:}/hf-cache"} - {name: HF_HUB_OFFLINE, value: "1"} - {name: TRANSFORMERS_OFFLINE, value: "1"} - {name: NCCL_DEBUG, value: WARN} @@ -83,11 +83,11 @@ _shared: - {containerPort: 6379, name: gcs-server} - {containerPort: 8265, name: dashboard} - {containerPort: 10001, name: client} - # Lustre PVC at /opt/nemo-rl (subPath hemild/rl-k8s) for code; at + # Lustre PVC at /opt/nemo-rl (subPath ${user:}/rl-k8s) for code; at # /mnt/rl-workspace (no subPath) for datasets, checkpoints, HF cache. codeMounts: &code_mounts - {mountPath: /dev/shm, name: dshm} - - {mountPath: /opt/nemo-rl, name: rl-workspace, subPath: hemild/rl-k8s} + - {mountPath: /opt/nemo-rl, name: rl-workspace, subPath: "${user:}/rl-k8s"} - {mountPath: /mnt/rl-workspace, name: rl-workspace} headVolumes: &head_volumes - name: dshm diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml index b716dd62b16..387afa2192e 100644 --- a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml @@ -11,13 +11,13 @@ # head + worker pod, served off the # shared FSx Lustre PVC via subPath. # -# Because /opt/nemo-rl is the Lustre checkout at hemild/rl-k8s, edits on Lustre +# Because /opt/nemo-rl is the Lustre checkout at ${user:}/rl-k8s, edits on Lustre # show up in the next submission without rebuilding the image. That's the whole # point of prod mode: iterate on code without churning the container. # # Prereqs (onboarding §3–§5 already applied in `default`): # - rl-workspace PVC bound (shared FSx Lustre, 2.4 TiB, RWX) -# - /mnt/rl-workspace/hemild/rl-k8s contains a git checkout of +# - /mnt/rl-workspace/${user:}/rl-k8s contains a git checkout of # https://github.com/NVIDIA-NeMo/RL on branch hemil/k8s-infra-cp # - nvcr-secret, wandb-api-key, nemo-rl-endpoint-registry all present # @@ -62,12 +62,12 @@ _shared: - {containerPort: 6379, name: gcs-server} - {containerPort: 8265, name: dashboard} - {containerPort: 10001, name: client} - # Volumes. The Lustre PVC is mounted at /opt/nemo-rl (subPath hemild/rl-k8s) + # Volumes. The Lustre PVC is mounted at /opt/nemo-rl (subPath ${user:}/rl-k8s) # for code, and at /mnt/rl-workspace (no subPath) for run outputs, caches, # and checkpoints. dshm stays as an emptyDir for Ray's object store. codeMounts: &code_mounts - {mountPath: /dev/shm, name: dshm} - - {mountPath: /opt/nemo-rl, name: rl-workspace, subPath: hemild/rl-k8s} + - {mountPath: /opt/nemo-rl, name: rl-workspace, subPath: "${user:}/rl-k8s"} - {mountPath: /mnt/rl-workspace, name: rl-workspace} headVolumes: &head_volumes - name: dshm @@ -110,7 +110,7 @@ launch: # Full instruction-following dataset (~20K prompts) snapshotted from # huggingface.co/datasets/nvidia/Nemotron-RL-instruction_following onto the # shared Lustre workspace. - IF_DATA=/mnt/rl-workspace/hemild/datasets/instruction_following/instruction_following.jsonl + IF_DATA=/mnt/rl-workspace/${user:}/datasets/instruction_following/instruction_following.jsonl export DISAGG_TRAIN_PATH=$IF_DATA export DISAGG_VALID_PATH=$IF_DATA export RAY_enable_infeasible_task_early_exit=true From 0c0032eef54f445611a0697655e7b7902c78ab8c Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Wed, 22 Apr 2026 22:44:17 -0700 Subject: [PATCH 42/84] feat(nrl-k8s): auto-manage DRA resources, default to --rayjob Signed-off-by: Terry Kong --- .../qwen3_30b_math_8n_4gpu.gb300.infra.yaml | 12 +- tools/nrl_k8s/src/nrl_k8s/cli.py | 50 ++++---- tools/nrl_k8s/src/nrl_k8s/k8s.py | 102 +++++++++++++++++ tools/nrl_k8s/src/nrl_k8s/manifest.py | 108 +++++++++++++++++- tools/nrl_k8s/src/nrl_k8s/orchestrate.py | 59 +++++++++- tools/nrl_k8s/src/nrl_k8s/rayjob.py | 3 +- tools/nrl_k8s/tests/unit/test_cli.py | 1 + 7 files changed, 300 insertions(+), 35 deletions(-) diff --git a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml index 1b09cc773c6..cbe2825c31c 100644 --- a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml @@ -2,13 +2,9 @@ # # Topology: 8 workers × 4 GPUs = 32 GPUs in a single RayCluster, scheduled by # KAI as one gang. NVLink/MNNVL spans all 32 GPUs via a ComputeDomain channel, -# and NCCL rides 8× RoCE NICs per node — both are attached through Dynamic -# Resource Allocation (DRA), so ResourceClaimTemplates must already exist: -# -# compute-domain-qwen3-30b-math — ComputeDomain channel (1 per pod), -# driver compute-domain.nvidia.com, -# allocationMode Single (shared across pods). -# roce-qwen3-30b-math — 8× roce.networking.k8s.aws per pod. +# and NCCL rides 8× RoCE NICs per node — both attached through DRA. The CLI +# auto-creates and auto-deletes the ComputeDomain + RoCE ResourceClaimTemplate +# based on the resourceClaims in the worker pod spec. # # Submission is prod-shaped (mirrors qwen3_4b_if_single.gb300.prod.infra.yaml): # submit.submitter portForward — Ray Job SDK via kubectl port-forward. @@ -23,8 +19,6 @@ # - /mnt/rl-workspace/${user:}/rl-k8s contains a git checkout of # https://github.com/NVIDIA-NeMo/RL on branch hemil/k8s-infra-cp. # - nvcr-secret, wandb-api-key, nemo-rl-endpoint-registry all present. -# - ResourceClaimTemplates compute-domain-qwen3-30b-math and -# roce-qwen3-30b-math exist in the `default` namespace. # - KAI topology `gb300-topology` is registered and advertises # nvidia.com/gpu.clique as a placement key. # diff --git a/tools/nrl_k8s/src/nrl_k8s/cli.py b/tools/nrl_k8s/src/nrl_k8s/cli.py index 1e5bf5bf0b8..06dafc492b2 100644 --- a/tools/nrl_k8s/src/nrl_k8s/cli.py +++ b/tools/nrl_k8s/src/nrl_k8s/cli.py @@ -213,7 +213,7 @@ def check( cluster = getattr(loaded.infra.clusters, role) if cluster is None: continue - manifests[role] = build_raycluster_manifest(cluster, loaded.infra) + manifests[role] = build_raycluster_manifest(cluster, loaded.infra, role=role) if output_path is not None: _dump_check_output(loaded, manifests, output_path, output_format) @@ -374,12 +374,13 @@ def validate(ctx, recipe, overrides, infra_path) -> None: "submit training — skip daemons on gym/generation roles.", ) @click.option( - "--rayjob", + "--rayjob/--raycluster", "as_rayjob", - is_flag=True, - help="Submit as an ephemeral KubeRay RayJob (auto-teardown) instead of " - "attaching to a long-lived RayCluster. Ignores --replace/--recreate/" - "--skip-daemons (they're not applicable).", + default=True, + help="--rayjob (default): submit as an ephemeral KubeRay RayJob that " + "auto-tears down the cluster when the job finishes. --raycluster: " + "attach to a long-lived RayCluster (supports --replace/--recreate/" + "--skip-daemons).", ) @click.option( "--rayjob-name", @@ -442,24 +443,24 @@ def run( cli_run_id: str | None, cli_wait: bool | None, ) -> None: - """Submit a recipe to the cluster. Long-lived by default, ephemeral with ``--rayjob``. - - **Long-lived mode (default)** — idempotent: for each declared role, - reuse the live RayCluster when its spec matches the rendered manifest, - apply when it is absent, warn + reuse on drift (pass ``--recreate`` to - delete + re-apply). Then submit daemons and the training entrypoint. - Cluster stays up for subsequent ``nrl-k8s run`` invocations. - ``--mode interactive`` (default) uses port-forward + working_dir upload - and tails logs; ``--mode batch`` uses kubectl exec + in-image code and - returns as soon as the driver is running via nohup. - - **Ephemeral mode (``--rayjob``)** — submits the recipe as a KubeRay - RayJob. KubeRay creates the RayCluster, submits + """Submit a recipe to the cluster. Ephemeral by default, long-lived with ``--raycluster``. + + **Ephemeral mode (``--rayjob``, default)** — submits the recipe as a + KubeRay RayJob. KubeRay creates the RayCluster, submits ``infra.launch.entrypoint`` over the dashboard HTTP API, polls until the driver is terminal, then tears the cluster down. ``shutdownAfterJobFinishes=true`` by default. Pass ``--no-wait`` to return as soon as the RayJob is applied, ``--dry-run`` to render the manifest without applying. + + **Long-lived mode (``--raycluster``)** — idempotent: for each declared + role, reuse the live RayCluster when its spec matches the rendered + manifest, apply when it is absent, warn + reuse on drift (pass + ``--recreate`` to delete + re-apply). Then submit daemons and the + training entrypoint. Cluster stays up for subsequent ``nrl-k8s run`` + invocations. ``--mode interactive`` (default) uses port-forward + + working_dir upload and tails logs; ``--mode batch`` uses kubectl exec + + in-image code and returns as soon as the driver is running via nohup. """ from . import orchestrate from . import submit as submit_mod @@ -539,6 +540,7 @@ def _run_rayjob( ) -> None: """``nrl-k8s run --rayjob`` path. KubeRay owns the RayCluster lifecycle.""" from . import k8s + from . import orchestrate from . import submit as submit_mod from .rayjob import build_rayjob_manifest @@ -547,6 +549,7 @@ def _run_rayjob( cluster, loaded.infra, entrypoint=loaded.infra.launch.entrypoint, + role="training", name=name, shutdown_after_finishes=shutdown_after, ttl_seconds_after_finished=ttl_seconds, @@ -561,6 +564,7 @@ def _run_rayjob( if not submit_mod.is_in_cluster(): _preflight_or_exit(namespace) + orchestrate.ensure_dra_resources("training", loaded, log=click.echo) click.echo(f"[run --rayjob] applying RayJob {job_name} in {namespace}") try: k8s.apply_rayjob(manifest, namespace) @@ -596,6 +600,7 @@ def _on_update(deployment: str | None, job: str | None) -> None: click.echo(f"[run --rayjob] {job_name} finished: deployment={dep} job={job_status}") if message: click.echo(f"[run --rayjob] message: {message}") + orchestrate.delete_dra_resources("training", loaded, log=click.echo) sys.exit(0 if dep == "Complete" else 1) @@ -756,7 +761,7 @@ def cluster_up( loaded = _load_or_exit(recipe, overrides, infra_path) cluster_spec = _pick_cluster_or_exit(loaded, role) if dry_run: - manifest = build_raycluster_manifest(cluster_spec, loaded.infra) + manifest = build_raycluster_manifest(cluster_spec, loaded.infra, role=role) click.echo(yaml.safe_dump(manifest, sort_keys=False).rstrip()) return @@ -836,6 +841,11 @@ def cluster_down( k8s.wait_for_raycluster_gone(target, namespace) click.echo(f"RayCluster {target} deleted.") + if role: + from . import orchestrate + + orchestrate.delete_dra_resources(role, loaded, log=click.echo) + @cluster.command("list") @click.option( diff --git a/tools/nrl_k8s/src/nrl_k8s/k8s.py b/tools/nrl_k8s/src/nrl_k8s/k8s.py index b7166827843..5c9e5609abe 100644 --- a/tools/nrl_k8s/src/nrl_k8s/k8s.py +++ b/tools/nrl_k8s/src/nrl_k8s/k8s.py @@ -302,6 +302,104 @@ def delete_configmap(name: str, namespace: str, *, ignore_missing: bool = True) raise +# ============================================================================= +# DRA: ComputeDomain + ResourceClaimTemplate +# ============================================================================= + +COMPUTE_DOMAIN_GROUP = "resource.nvidia.com" +COMPUTE_DOMAIN_VERSION = "v1beta1" +COMPUTE_DOMAIN_PLURAL = "computedomains" + +RCT_GROUP = "resource.k8s.io" +RCT_VERSION = "v1" +RCT_PLURAL = "resourceclaimtemplates" + + +def apply_compute_domain(manifest: dict[str, Any], namespace: str) -> dict[str, Any]: + """Create a ComputeDomain. No-op on 409 (already exists).""" + api = custom_objects_api() + try: + return with_retries( + lambda: api.create_namespaced_custom_object( + group=COMPUTE_DOMAIN_GROUP, + version=COMPUTE_DOMAIN_VERSION, + namespace=namespace, + plural=COMPUTE_DOMAIN_PLURAL, + body=manifest, + ) + ) + except ApiException as exc: + if exc.status == 409: + return {} + raise + + +def delete_compute_domain( + name: str, namespace: str, *, ignore_missing: bool = True +) -> None: + api = custom_objects_api() + try: + with_retries( + lambda: api.delete_namespaced_custom_object( + group=COMPUTE_DOMAIN_GROUP, + version=COMPUTE_DOMAIN_VERSION, + namespace=namespace, + plural=COMPUTE_DOMAIN_PLURAL, + name=name, + ) + ) + except ApiException as exc: + if exc.status == 404 and ignore_missing: + return + raise + + +def apply_resource_claim_template( + manifest: dict[str, Any], namespace: str +) -> dict[str, Any]: + """Create a ResourceClaimTemplate. No-op on 409 (already exists).""" + api = custom_objects_api() + try: + return with_retries( + lambda: api.create_namespaced_custom_object( + group=RCT_GROUP, + version=RCT_VERSION, + namespace=namespace, + plural=RCT_PLURAL, + body=manifest, + ) + ) + except ApiException as exc: + if exc.status == 409: + return {} + raise + + +def delete_resource_claim_template( + name: str, namespace: str, *, ignore_missing: bool = True +) -> None: + api = custom_objects_api() + try: + with_retries( + lambda: api.delete_namespaced_custom_object( + group=RCT_GROUP, + version=RCT_VERSION, + namespace=namespace, + plural=RCT_PLURAL, + name=name, + ) + ) + except ApiException as exc: + if exc.status == 404 and ignore_missing: + return + raise + + +# ============================================================================= +# Pod helpers +# ============================================================================= + + def get_head_pod(cluster_name: str, namespace: str) -> Any: """Return the first ``Running`` head pod for a RayCluster. @@ -328,12 +426,16 @@ def get_head_pod(cluster_name: str, namespace: str) -> Any: __all__ = [ + "apply_compute_domain", "apply_raycluster", "apply_rayjob", + "apply_resource_claim_template", "custom_objects_api", + "delete_compute_domain", "delete_configmap", "delete_raycluster", "delete_rayjob", + "delete_resource_claim_template", "get_head_pod", "get_raycluster", "get_rayjob", diff --git a/tools/nrl_k8s/src/nrl_k8s/manifest.py b/tools/nrl_k8s/src/nrl_k8s/manifest.py index 6c9c10b9c91..d30af4cd20b 100644 --- a/tools/nrl_k8s/src/nrl_k8s/manifest.py +++ b/tools/nrl_k8s/src/nrl_k8s/manifest.py @@ -24,7 +24,10 @@ def build_raycluster_manifest( - cluster: ClusterSpec, infra: InfraConfig + cluster: ClusterSpec, + infra: InfraConfig, + *, + role: str | None = None, ) -> dict[str, Any]: """Build the full RayCluster dict for apply. @@ -33,6 +36,10 @@ def build_raycluster_manifest( infra: top-level InfraConfig — supplies namespace, image, pull secrets, optional serviceAccount. These are patched into every container / pod template in the spec. + role: cluster role name (``training``, ``generation``, ``gym``). When + provided, DRA ``resourceClaimTemplateName`` references in worker + pods are rewritten to deterministic names derived from the cluster + name and role. Returns: A dict suitable for ``CustomObjectsApi.create_namespaced_custom_object``. @@ -43,6 +50,11 @@ def build_raycluster_manifest( _patch_image_pull_secrets(spec, list(infra.imagePullSecrets)) if infra.serviceAccount is not None: _patch_service_account(spec, infra.serviceAccount) + # DRA resources are named {prefix}-{cluster_name}-{role} so that + # disaggregated setups with multiple clusters get distinct + # ComputeDomains and RoCE templates per role. + if role is not None: + _rewrite_dra_template_names(spec, cluster.name, role) metadata: dict[str, Any] = { "name": cluster.name, @@ -101,4 +113,96 @@ def _patch_service_account(raycluster_spec: dict, service_account: str) -> None: pod_spec["serviceAccountName"] = service_account -__all__ = ["build_raycluster_manifest"] +# ============================================================================= +# DRA: detect and rewrite resourceClaimTemplateName in worker pods +# ============================================================================= + +_DRA_CLAIM_PREFIX: dict[str, str] = { + "compute-domain-channel": "compute-domain-", + "roce-channel": "roce-", +} + + +def _rewrite_dra_template_names( + spec: dict, cluster_name: str, role: str +) -> None: + """Rewrite ``resourceClaimTemplateName`` in worker pods to deterministic names.""" + for wg in spec.get("workerGroupSpecs") or []: + pod_spec = wg.get("template", {}).get("spec") + if not isinstance(pod_spec, dict): + continue + for claim in pod_spec.get("resourceClaims") or []: + prefix = _DRA_CLAIM_PREFIX.get(claim.get("name", "")) + if prefix: + claim["resourceClaimTemplateName"] = ( + f"{prefix}{cluster_name}-{role}" + ) + + +def dra_resources_for_cluster( + cluster_name: str, role: str, spec: dict +) -> list[tuple[str, str]]: + """Return ``[(kind, name), ...]`` for DRA resources a cluster spec needs. + + ``kind`` is ``"compute-domain"`` or ``"roce"``. Scans every worker pod + template for well-known claim names. + """ + found: list[tuple[str, str]] = [] + seen: set[str] = set() + for wg in spec.get("workerGroupSpecs") or []: + pod_spec = wg.get("template", {}).get("spec") + if not isinstance(pod_spec, dict): + continue + for claim in pod_spec.get("resourceClaims") or []: + claim_name = claim.get("name", "") + prefix = _DRA_CLAIM_PREFIX.get(claim_name) + if prefix and claim_name not in seen: + seen.add(claim_name) + resource_name = f"{prefix}{cluster_name}-{role}" + kind = "compute-domain" if "compute-domain" in prefix else "roce" + found.append((kind, resource_name)) + return found + + +def build_compute_domain_manifest(name: str, namespace: str) -> dict[str, Any]: + return { + "apiVersion": "resource.nvidia.com/v1beta1", + "kind": "ComputeDomain", + "metadata": {"name": name, "namespace": namespace}, + "spec": { + "channel": {"resourceClaimTemplate": {"name": name}}, + "numNodes": 0, + }, + } + + +def build_roce_template_manifest(name: str, namespace: str) -> dict[str, Any]: + # TODO: expose roce count via infra config — hardcoded to 8 for now + return { + "apiVersion": "resource.k8s.io/v1", + "kind": "ResourceClaimTemplate", + "metadata": {"name": name, "namespace": namespace}, + "spec": { + "spec": { + "devices": { + "requests": [ + { + "exactly": { + "count": 8, + "deviceClassName": "roce.networking.k8s.aws", + }, + "name": "roce", + } + ], + }, + }, + }, + } + + +__all__ = [ + "build_compute_domain_manifest", + "build_raycluster_manifest", + "build_roce_template_manifest", + "dra_resources_for_cluster", +] diff --git a/tools/nrl_k8s/src/nrl_k8s/orchestrate.py b/tools/nrl_k8s/src/nrl_k8s/orchestrate.py index 45a7567ecbb..8d88eb96607 100644 --- a/tools/nrl_k8s/src/nrl_k8s/orchestrate.py +++ b/tools/nrl_k8s/src/nrl_k8s/orchestrate.py @@ -32,7 +32,12 @@ from . import k8s, submit, workdir from .config import LoadedConfig, get_username -from .manifest import build_raycluster_manifest +from .manifest import ( + build_compute_domain_manifest, + build_raycluster_manifest, + build_roce_template_manifest, + dra_resources_for_cluster, +) from .schema import ClusterSpec, CodeSource, InfraConfig, SubmitterMode from .submitters import SubmissionHandle, build_submitter, save_handle @@ -74,10 +79,11 @@ def bring_up_cluster( ) -> str: """Apply the RayCluster for ``role`` and wait for it to be ready.""" cluster = _require_cluster(loaded.infra, role) - manifest = build_raycluster_manifest(cluster, loaded.infra) + manifest = build_raycluster_manifest(cluster, loaded.infra, role=role) name = cluster.name namespace = loaded.infra.namespace + ensure_dra_resources(role, loaded, log=log) log(f"[{role}] applying RayCluster {name} in namespace {namespace}") k8s.apply_raycluster(manifest, namespace) @@ -106,7 +112,7 @@ def ensure_cluster( and reuse anyway — pass ``recreate=True`` to delete + re-apply instead. """ cluster = _require_cluster(loaded.infra, role) - manifest = build_raycluster_manifest(cluster, loaded.infra) + manifest = build_raycluster_manifest(cluster, loaded.infra, role=role) name = cluster.name namespace = loaded.infra.namespace @@ -122,6 +128,7 @@ def ensure_cluster( f"'{live_owner}' (you are '{me}'). Use a different cluster " f"name or ask {live_owner} to tear it down." ) + ensure_dra_resources(role, loaded, log=log) if live is None: log(f"[{role}] applying RayCluster {name} in namespace {namespace}") k8s.apply_raycluster(manifest, namespace) @@ -181,6 +188,52 @@ def _strip_server_fields(obj): return obj +def ensure_dra_resources( + role: Role, + loaded: LoadedConfig, + *, + log: callable, +) -> None: + """Create ComputeDomain / RoCE ResourceClaimTemplate if the spec needs them.""" + cluster = _get_cluster(loaded.infra, role) + if cluster is None: + return + namespace = loaded.infra.namespace + resources = dra_resources_for_cluster(cluster.name, role, cluster.spec) + for kind, name in resources: + if kind == "compute-domain": + log(f"[{role}] ensuring ComputeDomain {name}") + k8s.apply_compute_domain( + build_compute_domain_manifest(name, namespace), namespace + ) + elif kind == "roce": + log(f"[{role}] ensuring RoCE ResourceClaimTemplate {name}") + k8s.apply_resource_claim_template( + build_roce_template_manifest(name, namespace), namespace + ) + + +def delete_dra_resources( + role: Role, + loaded: LoadedConfig, + *, + log: callable, +) -> None: + """Delete DRA resources for a role.""" + cluster = _get_cluster(loaded.infra, role) + if cluster is None: + return + namespace = loaded.infra.namespace + resources = dra_resources_for_cluster(cluster.name, role, cluster.spec) + for kind, name in reversed(resources): + if kind == "roce": + log(f"[{role}] deleting RoCE ResourceClaimTemplate {name}") + k8s.delete_resource_claim_template(name, namespace) + elif kind == "compute-domain": + log(f"[{role}] deleting ComputeDomain {name}") + k8s.delete_compute_domain(name, namespace) + + def submit_daemon( role: Role, loaded: LoadedConfig, diff --git a/tools/nrl_k8s/src/nrl_k8s/rayjob.py b/tools/nrl_k8s/src/nrl_k8s/rayjob.py index 27ae4de0976..effdd4aaa7b 100644 --- a/tools/nrl_k8s/src/nrl_k8s/rayjob.py +++ b/tools/nrl_k8s/src/nrl_k8s/rayjob.py @@ -29,6 +29,7 @@ def build_rayjob_manifest( infra: InfraConfig, *, entrypoint: str, + role: str = "training", name: str | None = None, shutdown_after_finishes: bool = True, ttl_seconds_after_finished: int = DEFAULT_TTL_SECONDS, @@ -64,7 +65,7 @@ def build_rayjob_manifest( # Reuse the RayCluster builder so image / imagePullSecrets / SA / labels # are patched the same way as the standalone cluster path. Then lift # the .spec out; RayJob nests it under rayClusterSpec. - cluster_manifest = build_raycluster_manifest(cluster, infra) + cluster_manifest = build_raycluster_manifest(cluster, infra, role=role) ray_cluster_spec = cluster_manifest["spec"] job_name = name or cluster.name diff --git a/tools/nrl_k8s/tests/unit/test_cli.py b/tools/nrl_k8s/tests/unit/test_cli.py index 53cc8a78116..7593e400b0a 100644 --- a/tools/nrl_k8s/tests/unit/test_cli.py +++ b/tools/nrl_k8s/tests/unit/test_cli.py @@ -420,6 +420,7 @@ def _fake_run(loaded, *, log, repo_root, replace, run_id, skip_daemons, recreate [ "run", str(recipe), + "--raycluster", "--mode", "batch", "--code-source", "image", "--code-path", "/opt/nemo-rl", From ae293c80b094cd473c0d732d607e38d40fa6c034 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Wed, 22 Apr 2026 23:22:01 -0700 Subject: [PATCH 43/84] docs(nrl-k8s): fix stale README (launch->run, --follow->--wait, default --rayjob) Signed-off-by: Terry Kong --- tools/nrl_k8s/README.md | 75 ++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 46 deletions(-) diff --git a/tools/nrl_k8s/README.md b/tools/nrl_k8s/README.md index d200142ebc9..87acf439530 100644 --- a/tools/nrl_k8s/README.md +++ b/tools/nrl_k8s/README.md @@ -90,7 +90,7 @@ between roles, no endpoint-registry rendezvous. nrl-k8s run \ tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ - --follow + --wait ``` ### `qwen3_4b_if_gym_disagg` — gym on its own cluster @@ -102,9 +102,10 @@ RayCluster runs the gym rollout server. nrl-k8s run \ tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml \ - --follow + --raycluster --wait ``` +`--raycluster` is required for disaggregated runs (multiple clusters). `run` applies both RayCluster manifests in order, submits the gym daemon once its cluster is `Ready`, then submits the training Ray Job against the training cluster and tails its logs. @@ -124,14 +125,14 @@ the standalone generation server, which lives on its own GPUs: nrl-k8s run \ tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ - --follow + --raycluster --wait ``` -`run` walks the three roles in order: `generation` first (vLLM has to be +`run --raycluster` walks the three roles in order: `generation` first (vLLM has to be serving before training opens sockets to it), then `gym` (publishes `gym_head_server` into the endpoint-registry ConfigMap), then `training`. Once the training Ray Job is submitted its auto-generated ID is printed and -`--follow` tails its logs via a port-forward to the training dashboard. +`--wait` tails its logs until the job reaches a terminal state. ```bash nrl-k8s status \ @@ -238,42 +239,24 @@ nrl-k8s cluster up \ --role training --dry-run ``` -### `nrl-k8s launch` - -Submit a training Ray Job against an **already-up** training cluster. Does -not bring up generation or gym. Use this when `nrl-k8s run` has already -stood things up and you just want to rerun training after editing code. - -```bash -nrl-k8s launch \ - tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ - --follow --replace -``` - -Flags: `--repo-root ` (defaults to `cwd`; the source tree Ray packages -into `working_dir`), `--follow`, `--replace` (see below). - ### `nrl-k8s run` -Do the full sequence: apply each RayCluster, submit each role's daemon -(for generation / gym), then submit the training Ray Job. Same flags as -`launch`. Safe to re-run idempotently on healthy clusters — already-running -daemons are skipped unless `--replace` is passed. +Submit a recipe to the cluster. Defaults to ephemeral **RayJob mode** +(`--rayjob`): KubeRay creates the RayCluster, submits the entrypoint, +polls until terminal, then tears down the cluster automatically. DRA +resources (ComputeDomain, RoCE ResourceClaimTemplate) are auto-created +before the job and auto-deleted after it finishes. -### `nrl-k8s cluster up --role ` - -Apply one RayCluster manifest and, once `Ready`, submit its daemon if the -recipe has one. - -```bash -nrl-k8s cluster up \ - tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ - --role gym -``` +Pass `--raycluster` for **long-lived mode**: idempotently bring up each +declared RayCluster, submit daemons (generation / gym), then submit +training. Clusters stay up for subsequent runs. Use `--replace` to stop +running jobs before resubmitting, `--recreate` to delete + re-apply +drifted clusters. -Flags: `--wait/--no-wait`, `--timeout ` (default 900). +Flags: `--wait/--no-wait`, `--dry-run` (RayJob mode only), +`--replace`, `--recreate`, `--skip-daemons` (long-lived mode only), +`--run-id `, `--mode {interactive, batch}`, +`--code-source {upload, image, lustre}`, `--code-path `. ### `nrl-k8s cluster down` @@ -336,15 +319,15 @@ auto-managed. Stop a Ray Job by submission id. Useful for clearing a stuck training job before a re-run (though `launch --replace` does this automatically). -### `nrl-k8s dev` / `nrl-k8s dashboard` / `nrl-k8s doctor` +### `nrl-k8s dev` / `nrl-k8s doctor` Not yet implemented — stubs print `not yet implemented (phase: ...)` and exit `2`. ## Modes: interactive vs batch -`launch` and `run` both take `--mode {interactive, batch}`. The flag is a -macro — it flips a coherent set of defaults that a researcher would +`nrl-k8s run --raycluster` takes `--mode {interactive, batch}`. The flag is +a macro — it flips a coherent set of defaults that a researcher would otherwise pick individually. | dimension | `--mode interactive` (default) | `--mode batch` | @@ -389,7 +372,7 @@ nrl-k8s cluster up "$RECIPE" --infra "$INFRA" --role training # Researcher, per run — returns in seconds, laptop can close. RUN_ID=qwen3-4b-gym-disagg-$(date +%Y%m%d-%H%M%S) -nrl-k8s launch "$RECIPE" --infra "$INFRA" --run-id "$RUN_ID" +nrl-k8s run "$RECIPE" --infra "$INFRA" --raycluster --run-id "$RUN_ID" # run id: qwen3-4b-gym-disagg-20260421-103012 # kind: exec # cluster: raycluster-gym-disagg-qwen3-4b (ns=nemo-rl-testing) @@ -406,7 +389,7 @@ nrl-k8s job stop "$RUN_ID" "$RECIPE" --infra "$INFRA" --role training The prod infra declares `submit.submitter: exec` + `launch.runMode: batch` + `launch.codeSource: image`, so `--mode batch` is implicit. The dev infra (`qwen3_4b_if_gym_disagg.infra.yaml`) keeps the -port-forward + upload path, letting `launch` / `run` default to +port-forward + upload path, letting `run --raycluster` default to foreground log tailing for dev iteration. The exec submitter writes a launcher script onto the head pod, runs it @@ -511,8 +494,8 @@ etc.) — the CLI does not inject `cd` for you. ## `--replace` semantics -Both `nrl-k8s launch` and `nrl-k8s run` accept `--replace`. It performs -three idempotency-relevant actions before submitting: +`nrl-k8s run --raycluster` accepts `--replace`. It performs three +idempotency-relevant actions before submitting: 1. **Endpoint registry reset.** The CLI parses the gym daemon's `--job-id` flag (see `tools/nrl_k8s/src/nrl_k8s/orchestrate.py:231`) and @@ -544,8 +527,8 @@ Ray's Job SDK caps `working_dir` at 100 MiB. `infra.launch.rayUploadPaths` you ship. The disagg example lists individual files under `resources_servers/instruction_following/` so the 87 MiB `train.jsonl` isn't included (see `qwen3_4b_if_full_disagg.infra.yaml:229`). If uploads are slow, -`nrl-k8s validate ... --show-recipe` won't help — instead `ls -lh` the -staged tmpdir by running `nrl-k8s launch --follow` and inspecting the log +`nrl-k8s check` won't help — instead `ls -lh` the +staged tmpdir by running `nrl-k8s run --raycluster --wait` and inspecting the log line `[training] staging working_dir ...` (look at `tools/nrl_k8s/src/nrl_k8s/workdir.py` for defaults). From 62538d395573dfedfa3ed71e1df98b12917147c8 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 00:15:50 -0700 Subject: [PATCH 44/84] feat(nrl-k8s): implement dev command (connect, stop, setup-secrets) Signed-off-by: Terry Kong --- tools/nrl_k8s/src/nrl_k8s/cli.py | 154 +++++++++++++++++++++++++++++++ tools/nrl_k8s/src/nrl_k8s/dev.py | 84 +++++++++++++++++ tools/nrl_k8s/src/nrl_k8s/k8s.py | 100 ++++++++++++++++++++ 3 files changed, 338 insertions(+) create mode 100644 tools/nrl_k8s/src/nrl_k8s/dev.py diff --git a/tools/nrl_k8s/src/nrl_k8s/cli.py b/tools/nrl_k8s/src/nrl_k8s/cli.py index 06dafc492b2..ba1eb35f419 100644 --- a/tools/nrl_k8s/src/nrl_k8s/cli.py +++ b/tools/nrl_k8s/src/nrl_k8s/cli.py @@ -1160,6 +1160,160 @@ def job_stop( click.echo(f"stopped {submission_id}") +# ============================================================================= +# Dev pod +# ============================================================================= + + +@main.group() +def dev(): + """Manage a lightweight dev pod on the cluster.""" + + +@dev.command("connect") +@click.option( + "--image", + default="nvcr.io/nvidian/nemo-rl:nightly", + help="Container image for the dev pod.", +) +@click.option("--namespace", "-n", default=None, help="Kubernetes namespace.") +def dev_connect(image: str, namespace: str | None) -> None: + """Create a dev pod (if needed) and exec into it.""" + import subprocess + import time + + from . import k8s + from .config import get_username + from .dev import build_dev_pod_manifest + + user = get_username() + pod_name = f"{user}-dev-pod" + if namespace is None: + namespace = _infer_namespace() + + phase = k8s.get_pod_phase(pod_name, namespace) + if phase is None: + click.echo(f"creating dev pod {pod_name} in {namespace} ...") + manifest = build_dev_pod_manifest(user, namespace, image) + k8s.create_pod(manifest, namespace) + phase = "Pending" + + if phase != "Running": + click.echo(f"waiting for {pod_name} to be Running ...") + for _ in range(120): + time.sleep(2) + phase = k8s.get_pod_phase(pod_name, namespace) + if phase == "Running": + break + if phase in ("Failed", "Succeeded"): + _cli_error(f"dev pod reached phase {phase} — check `kubectl describe pod {pod_name} -n {namespace}`") + else: + _cli_error(f"dev pod did not reach Running after 240s (phase={phase})") + + click.echo(f"connecting to {pod_name} ...") + subprocess.run( + ["kubectl", "exec", "-it", "-n", namespace, pod_name, "--", "bash"], + ) + click.echo(f"\npod {pod_name} is still running — stop with: nrl-k8s dev stop") + + +@dev.command("stop") +@click.option("--namespace", "-n", default=None, help="Kubernetes namespace.") +def dev_stop(namespace: str | None) -> None: + """Delete your dev pod.""" + from . import k8s + from .config import get_username + + user = get_username() + pod_name = f"{user}-dev-pod" + if namespace is None: + namespace = _infer_namespace() + + phase = k8s.get_pod_phase(pod_name, namespace) + if phase is None: + click.echo(f"no dev pod {pod_name} found in {namespace}") + return + + click.echo(f"deleting {pod_name} ...") + k8s.delete_pod(pod_name, namespace) + click.echo(f"{pod_name} deleted.") + + +_REQUIRED_FIRST_TIME = ("HF_TOKEN", "WANDB_API_KEY") + + +@dev.command("setup-secrets") +@click.argument("kvs", nargs=-1) +@click.option( + "--ssh-key", + type=click.Path(exists=True), + multiple=True, + help="Path to an SSH private key (repeatable).", +) +@click.option("--namespace", "-n", default=None, help="Kubernetes namespace.") +def dev_setup_secrets( + kvs: tuple[str, ...], ssh_key: tuple[str, ...], namespace: str | None +) -> None: + """Create or update your user secrets. + + Pass token values as NAME=VAL positional args and SSH keys via --ssh-key. + + First-time usage requires HF_TOKEN, WANDB_API_KEY, and --ssh-key: + + \b + nrl-k8s dev setup-secrets \\ + HF_TOKEN=hf_xxx WANDB_API_KEY=key_yyy \\ + --ssh-key ~/.ssh/id_ed25519 + + Subsequent runs accept any subset to update individual keys. + """ + from pathlib import Path + + from . import k8s + from .config import get_username + + user = get_username() + secret_name = f"{user}-secrets" + if namespace is None: + namespace = _infer_namespace() + + data: dict[str, str] = {} + for kv in kvs: + if "=" not in kv: + _cli_error(f"invalid argument {kv!r} — expected NAME=VAL") + name, val = kv.split("=", 1) + data[name] = val + + for key_path in ssh_key: + p = Path(key_path) + data["SSH_KEY_NAME"] = p.name + data["SSH_KEY_CONTENT"] = p.read_text() + + is_new = not k8s.secret_exists(secret_name, namespace) + if is_new: + missing = [k for k in _REQUIRED_FIRST_TIME if k not in data] + if missing: + _cli_error( + f"first-time setup requires: {', '.join(missing)}", + hint=f"nrl-k8s dev setup-secrets {' '.join(f'{k}=' for k in missing)} --ssh-key ~/.ssh/id_ed25519", + ) + if not ssh_key: + _cli_error( + "first-time setup requires --ssh-key", + hint="nrl-k8s dev setup-secrets ... --ssh-key ~/.ssh/id_ed25519", + ) + + k8s.create_or_update_secret(secret_name, namespace, data) + action = "created" if is_new else "updated" + click.echo(f"{action} secret {secret_name} in {namespace} (keys: {', '.join(sorted(data))})") + + +def _infer_namespace() -> str: + from .config import _infer_kube_namespace + + return _infer_kube_namespace() + + # ============================================================================= # Helpers # ============================================================================= diff --git a/tools/nrl_k8s/src/nrl_k8s/dev.py b/tools/nrl_k8s/src/nrl_k8s/dev.py new file mode 100644 index 00000000000..bd6de9e2b93 --- /dev/null +++ b/tools/nrl_k8s/src/nrl_k8s/dev.py @@ -0,0 +1,84 @@ +"""Dev pod manifest builder for ``nrl-k8s dev``.""" + +from __future__ import annotations + +from typing import Any + +_DEFAULT_IMAGE = "nvcr.io/nvidian/nemo-rl:nightly" +_DEFAULT_IMAGE_PULL_SECRET = "nvcr-secret" +_PVC_NAME = "rl-workspace" +_MOUNT_PATH = "/mnt/rl-workspace" + + +def build_dev_pod_manifest( + username: str, + namespace: str, + image: str = _DEFAULT_IMAGE, +) -> dict[str, Any]: + user_dir = f"{_MOUNT_PATH}/{username}" + secret_name = f"{username}-secrets" + pod_name = f"{username}-dev-pod" + + command = ( + f"mkdir -p {user_dir} /root/.ssh && " + 'if [ -n "$SSH_KEY_CONTENT" ]; then ' + 'printf "%s\\n" "$SSH_KEY_CONTENT" > /root/.ssh/$SSH_KEY_NAME && ' + "chmod 600 /root/.ssh/$SSH_KEY_NAME; " + "fi && " + "sleep infinity" + ) + + return { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": pod_name, + "namespace": namespace, + "labels": { + "nrl-k8s/owner": username, + "nrl-k8s/component": "dev-pod", + }, + }, + "spec": { + "restartPolicy": "Never", + "imagePullSecrets": [{"name": _DEFAULT_IMAGE_PULL_SECRET}], + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "nvidia.com/gpu.product", + "operator": "DoesNotExist", + } + ] + } + ] + } + } + }, + "containers": [ + { + "name": "dev", + "image": image, + "command": ["sh", "-c", command], + "workingDir": user_dir, + "envFrom": [{"secretRef": {"name": secret_name, "optional": True}}], + "resources": { + "requests": {"cpu": "100m", "memory": "256Mi"}, + "limits": {"cpu": "5", "memory": "10Gi"}, + }, + "volumeMounts": [ + {"name": "rl-workspace", "mountPath": _MOUNT_PATH}, + ], + } + ], + "volumes": [ + { + "name": "rl-workspace", + "persistentVolumeClaim": {"claimName": _PVC_NAME}, + }, + ], + }, + } diff --git a/tools/nrl_k8s/src/nrl_k8s/k8s.py b/tools/nrl_k8s/src/nrl_k8s/k8s.py index 5c9e5609abe..184174646d1 100644 --- a/tools/nrl_k8s/src/nrl_k8s/k8s.py +++ b/tools/nrl_k8s/src/nrl_k8s/k8s.py @@ -425,22 +425,122 @@ def get_head_pod(cluster_name: str, namespace: str) -> Any: ) +def create_pod(manifest: dict[str, Any], namespace: str) -> dict[str, Any]: + """Create a pod. No-op on 409 (already exists).""" + load_kubeconfig() + core = client.CoreV1Api() + try: + return with_retries( + lambda: core.create_namespaced_pod(namespace=namespace, body=manifest) + ) + except ApiException as exc: + if exc.status == 409: + return {} + raise + + +def delete_pod(name: str, namespace: str, *, ignore_missing: bool = True) -> None: + load_kubeconfig() + core = client.CoreV1Api() + try: + with_retries( + lambda: core.delete_namespaced_pod(name=name, namespace=namespace) + ) + except ApiException as exc: + if exc.status == 404 and ignore_missing: + return + raise + + +def get_pod_phase(name: str, namespace: str) -> str | None: + """Return the pod phase (Pending/Running/Succeeded/Failed) or None if not found.""" + load_kubeconfig() + core = client.CoreV1Api() + try: + pod = with_retries( + lambda: core.read_namespaced_pod(name=name, namespace=namespace) + ) + return pod.status.phase if pod.status else None + except ApiException as exc: + if exc.status == 404: + return None + raise + + +# ============================================================================= +# Secrets +# ============================================================================= + + +def create_or_update_secret( + name: str, namespace: str, data: dict[str, str] +) -> None: + """Create a Secret or merge new keys into an existing one.""" + import base64 + + load_kubeconfig() + core = client.CoreV1Api() + encoded = {k: base64.b64encode(v.encode()).decode() for k, v in data.items()} + + try: + existing = core.read_namespaced_secret(name=name, namespace=namespace) + merged = dict(existing.data or {}) + merged.update(encoded) + existing.data = merged + with_retries( + lambda: core.replace_namespaced_secret( + name=name, namespace=namespace, body=existing + ) + ) + except ApiException as exc: + if exc.status == 404: + secret = client.V1Secret( + metadata=client.V1ObjectMeta(name=name, namespace=namespace), + type="Opaque", + data=encoded, + ) + with_retries( + lambda: core.create_namespaced_secret( + namespace=namespace, body=secret + ) + ) + else: + raise + + +def secret_exists(name: str, namespace: str) -> bool: + load_kubeconfig() + core = client.CoreV1Api() + try: + core.read_namespaced_secret(name=name, namespace=namespace) + return True + except ApiException as exc: + if exc.status == 404: + return False + raise + + __all__ = [ "apply_compute_domain", "apply_raycluster", "apply_rayjob", "apply_resource_claim_template", + "create_or_update_secret", + "create_pod", "custom_objects_api", "delete_compute_domain", "delete_configmap", + "delete_pod", "delete_raycluster", "delete_rayjob", "delete_resource_claim_template", "get_head_pod", + "get_pod_phase", "get_raycluster", "get_rayjob", "list_rayclusters", "load_kubeconfig", + "secret_exists", "wait_for_raycluster_gone", "wait_for_raycluster_ready", "wait_for_rayjob_terminal", From 0e8adeff6522d6fe7981930a4f4d37992226db52 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 00:50:46 -0700 Subject: [PATCH 45/84] docs(nrl-k8s): add dev command to README, rename subPath rl-k8s to nemo-rl Signed-off-by: Terry Kong --- tools/nrl_k8s/README.md | 45 +++++++++++++++++-- .../qwen3_30b_math_8n_4gpu.gb300.infra.yaml | 8 ++-- .../qwen3_4b_if_single.gb300.prod.infra.yaml | 8 ++-- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/tools/nrl_k8s/README.md b/tools/nrl_k8s/README.md index 87acf439530..4bfc577e601 100644 --- a/tools/nrl_k8s/README.md +++ b/tools/nrl_k8s/README.md @@ -319,10 +319,49 @@ auto-managed. Stop a Ray Job by submission id. Useful for clearing a stuck training job before a re-run (though `launch --replace` does this automatically). -### `nrl-k8s dev` / `nrl-k8s doctor` +### `nrl-k8s dev` -Not yet implemented — stubs print `not yet implemented (phase: ...)` and -exit `2`. +Lightweight dev pod for setup tasks (cloning repos, downloading models, +debugging). The pod runs on a CPU node with the shared workspace PVC +mounted. + +#### `nrl-k8s dev setup-secrets` + +Create or update your user secrets (tokens + SSH key). Required before +first `dev connect`: + +```bash +nrl-k8s dev setup-secrets \ + HF_TOKEN=hf_xxx WANDB_API_KEY=key_yyy \ + --ssh-key ~/.ssh/id_ed25519 +``` + +First-time usage requires `HF_TOKEN`, `WANDB_API_KEY`, and `--ssh-key`. +Subsequent runs accept any subset to update individual keys. + +#### `nrl-k8s dev connect` + +Create the dev pod (if it doesn't exist) and exec into it: + +```bash +nrl-k8s dev connect +``` + +Lands you in `/mnt/rl-workspace/` with your tokens as env vars +and SSH key at `/root/.ssh/`. The pod stays running after you exit — reconnect +with `dev connect` again. + +#### `nrl-k8s dev stop` + +Delete the dev pod: + +```bash +nrl-k8s dev stop +``` + +### `nrl-k8s doctor` + +Not yet implemented. ## Modes: interactive vs batch diff --git a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml index cbe2825c31c..1ad146dcb47 100644 --- a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml @@ -11,12 +11,12 @@ # launch.runMode batch — CLI returns after Ray accepts the job. # launch.codeSource image — no working_dir upload. # launch.codePath /opt/nemo-rl — RL repo served off the shared FSx -# Lustre PVC at subPath=${user:}/rl-k8s. +# Lustre PVC at subPath=${user:}/nemo-rl. # Edits on Lustre take effect without rebuilding the image. # # Prereqs (onboarding §3–§5 already applied in `default`): # - rl-workspace PVC bound (shared FSx Lustre, 2.4 TiB, RWX). -# - /mnt/rl-workspace/${user:}/rl-k8s contains a git checkout of +# - /mnt/rl-workspace/${user:}/nemo-rl contains a git checkout of # https://github.com/NVIDIA-NeMo/RL on branch hemil/k8s-infra-cp. # - nvcr-secret, wandb-api-key, nemo-rl-endpoint-registry all present. # - KAI topology `gb300-topology` is registered and advertises @@ -77,11 +77,11 @@ _shared: - {containerPort: 6379, name: gcs-server} - {containerPort: 8265, name: dashboard} - {containerPort: 10001, name: client} - # Lustre PVC at /opt/nemo-rl (subPath ${user:}/rl-k8s) for code; at + # Lustre PVC at /opt/nemo-rl (subPath ${user:}/nemo-rl) for code; at # /mnt/rl-workspace (no subPath) for datasets, checkpoints, HF cache. codeMounts: &code_mounts - {mountPath: /dev/shm, name: dshm} - - {mountPath: /opt/nemo-rl, name: rl-workspace, subPath: "${user:}/rl-k8s"} + - {mountPath: /opt/nemo-rl, name: rl-workspace, subPath: "${user:}/nemo-rl"} - {mountPath: /mnt/rl-workspace, name: rl-workspace} headVolumes: &head_volumes - name: dshm diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml index 387afa2192e..acae3b9f9c0 100644 --- a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml @@ -11,13 +11,13 @@ # head + worker pod, served off the # shared FSx Lustre PVC via subPath. # -# Because /opt/nemo-rl is the Lustre checkout at ${user:}/rl-k8s, edits on Lustre +# Because /opt/nemo-rl is the Lustre checkout at ${user:}/nemo-rl, edits on Lustre # show up in the next submission without rebuilding the image. That's the whole # point of prod mode: iterate on code without churning the container. # # Prereqs (onboarding §3–§5 already applied in `default`): # - rl-workspace PVC bound (shared FSx Lustre, 2.4 TiB, RWX) -# - /mnt/rl-workspace/${user:}/rl-k8s contains a git checkout of +# - /mnt/rl-workspace/${user:}/nemo-rl contains a git checkout of # https://github.com/NVIDIA-NeMo/RL on branch hemil/k8s-infra-cp # - nvcr-secret, wandb-api-key, nemo-rl-endpoint-registry all present # @@ -62,12 +62,12 @@ _shared: - {containerPort: 6379, name: gcs-server} - {containerPort: 8265, name: dashboard} - {containerPort: 10001, name: client} - # Volumes. The Lustre PVC is mounted at /opt/nemo-rl (subPath ${user:}/rl-k8s) + # Volumes. The Lustre PVC is mounted at /opt/nemo-rl (subPath ${user:}/nemo-rl) # for code, and at /mnt/rl-workspace (no subPath) for run outputs, caches, # and checkpoints. dshm stays as an emptyDir for Ray's object store. codeMounts: &code_mounts - {mountPath: /dev/shm, name: dshm} - - {mountPath: /opt/nemo-rl, name: rl-workspace, subPath: "${user:}/rl-k8s"} + - {mountPath: /opt/nemo-rl, name: rl-workspace, subPath: "${user:}/nemo-rl"} - {mountPath: /mnt/rl-workspace, name: rl-workspace} headVolumes: &head_volumes - name: dshm From 192c9fe629ab64f8fbbe7e3a50b00adb48d0a1b8 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 01:03:44 -0700 Subject: [PATCH 46/84] feat(nrl-k8s): add app.kubernetes.io/managed-by label to all resources Signed-off-by: Terry Kong --- tools/nrl_k8s/src/nrl_k8s/dev.py | 1 + tools/nrl_k8s/src/nrl_k8s/k8s.py | 6 +++++- tools/nrl_k8s/src/nrl_k8s/manifest.py | 11 ++++++++--- tools/nrl_k8s/src/nrl_k8s/rayjob.py | 4 +++- tools/nrl_k8s/tests/unit/test_manifest.py | 11 +++++++---- tools/nrl_k8s/tests/unit/test_rayjob.py | 14 +++++++------- 6 files changed, 31 insertions(+), 16 deletions(-) diff --git a/tools/nrl_k8s/src/nrl_k8s/dev.py b/tools/nrl_k8s/src/nrl_k8s/dev.py index bd6de9e2b93..c4dd18c634e 100644 --- a/tools/nrl_k8s/src/nrl_k8s/dev.py +++ b/tools/nrl_k8s/src/nrl_k8s/dev.py @@ -35,6 +35,7 @@ def build_dev_pod_manifest( "name": pod_name, "namespace": namespace, "labels": { + "app.kubernetes.io/managed-by": "nrl-k8s", "nrl-k8s/owner": username, "nrl-k8s/component": "dev-pod", }, diff --git a/tools/nrl_k8s/src/nrl_k8s/k8s.py b/tools/nrl_k8s/src/nrl_k8s/k8s.py index 184174646d1..d3a1c960bd1 100644 --- a/tools/nrl_k8s/src/nrl_k8s/k8s.py +++ b/tools/nrl_k8s/src/nrl_k8s/k8s.py @@ -495,7 +495,11 @@ def create_or_update_secret( except ApiException as exc: if exc.status == 404: secret = client.V1Secret( - metadata=client.V1ObjectMeta(name=name, namespace=namespace), + metadata=client.V1ObjectMeta( + name=name, + namespace=namespace, + labels={"app.kubernetes.io/managed-by": "nrl-k8s"}, + ), type="Opaque", data=encoded, ) diff --git a/tools/nrl_k8s/src/nrl_k8s/manifest.py b/tools/nrl_k8s/src/nrl_k8s/manifest.py index d30af4cd20b..0fe01b0deaa 100644 --- a/tools/nrl_k8s/src/nrl_k8s/manifest.py +++ b/tools/nrl_k8s/src/nrl_k8s/manifest.py @@ -18,6 +18,11 @@ from .schema import ClusterSpec, InfraConfig +# Every resource the CLI creates carries this label so admins can find +# orphans not managed by the tool: +# kubectl get rayclusters -l '!app.kubernetes.io/managed-by' +_MANAGED_BY_LABEL = {"app.kubernetes.io/managed-by": "nrl-k8s"} + # ============================================================================= # Public API # ============================================================================= @@ -60,7 +65,7 @@ def build_raycluster_manifest( "name": cluster.name, "namespace": infra.namespace, } - labels = {**infra.labels, **cluster.labels} + labels = {**_MANAGED_BY_LABEL, **infra.labels, **cluster.labels} annotations = {**infra.annotations, **cluster.annotations} if labels: metadata["labels"] = labels @@ -168,7 +173,7 @@ def build_compute_domain_manifest(name: str, namespace: str) -> dict[str, Any]: return { "apiVersion": "resource.nvidia.com/v1beta1", "kind": "ComputeDomain", - "metadata": {"name": name, "namespace": namespace}, + "metadata": {"name": name, "namespace": namespace, "labels": {**_MANAGED_BY_LABEL}}, "spec": { "channel": {"resourceClaimTemplate": {"name": name}}, "numNodes": 0, @@ -181,7 +186,7 @@ def build_roce_template_manifest(name: str, namespace: str) -> dict[str, Any]: return { "apiVersion": "resource.k8s.io/v1", "kind": "ResourceClaimTemplate", - "metadata": {"name": name, "namespace": namespace}, + "metadata": {"name": name, "namespace": namespace, "labels": {**_MANAGED_BY_LABEL}}, "spec": { "spec": { "devices": { diff --git a/tools/nrl_k8s/src/nrl_k8s/rayjob.py b/tools/nrl_k8s/src/nrl_k8s/rayjob.py index effdd4aaa7b..ccff904f90a 100644 --- a/tools/nrl_k8s/src/nrl_k8s/rayjob.py +++ b/tools/nrl_k8s/src/nrl_k8s/rayjob.py @@ -73,7 +73,9 @@ def build_rayjob_manifest( "name": job_name, "namespace": infra.namespace, } - merged_labels = {**infra.labels, **cluster.labels, **(extra_labels or {})} + from .manifest import _MANAGED_BY_LABEL + + merged_labels = {**_MANAGED_BY_LABEL, **infra.labels, **cluster.labels, **(extra_labels or {})} if merged_labels: metadata["labels"] = merged_labels merged_annotations = {**infra.annotations, **cluster.annotations} diff --git a/tools/nrl_k8s/tests/unit/test_manifest.py b/tools/nrl_k8s/tests/unit/test_manifest.py index ae40deccd6b..a44825efbfb 100644 --- a/tools/nrl_k8s/tests/unit/test_manifest.py +++ b/tools/nrl_k8s/tests/unit/test_manifest.py @@ -64,17 +64,20 @@ def test_labels_merged_from_infra_and_cluster(self) -> None: cluster = _make_cluster(labels={"role": "training"}) infra = _make_infra(labels={"team": "rl"}) got = build_raycluster_manifest(cluster, infra) - assert got["metadata"]["labels"] == {"role": "training", "team": "rl"} + labels = got["metadata"]["labels"] + assert labels["role"] == "training" + assert labels["team"] == "rl" + assert labels["app.kubernetes.io/managed-by"] == "nrl-k8s" def test_cluster_labels_win_on_collision(self) -> None: cluster = _make_cluster(labels={"team": "override"}) infra = _make_infra(labels={"team": "rl"}) got = build_raycluster_manifest(cluster, infra) - assert got["metadata"]["labels"] == {"team": "override"} + assert got["metadata"]["labels"]["team"] == "override" - def test_no_labels_key_when_empty(self) -> None: + def test_managed_by_label_always_present(self) -> None: got = build_raycluster_manifest(_make_cluster(), _make_infra()) - assert "labels" not in got["metadata"] + assert got["metadata"]["labels"]["app.kubernetes.io/managed-by"] == "nrl-k8s" # ============================================================================= diff --git a/tools/nrl_k8s/tests/unit/test_rayjob.py b/tools/nrl_k8s/tests/unit/test_rayjob.py index f0fc1744d9d..c99ce96f8dc 100644 --- a/tools/nrl_k8s/tests/unit/test_rayjob.py +++ b/tools/nrl_k8s/tests/unit/test_rayjob.py @@ -119,11 +119,11 @@ def test_labels_merged_from_infra_cluster_and_extra(self) -> None: got = build_rayjob_manifest( cluster, infra, entrypoint="x", extra_labels={"run-id": "r-1"} ) - assert got["metadata"]["labels"] == { - "role": "training", - "team": "rl", - "run-id": "r-1", - } + labels = got["metadata"]["labels"] + assert labels["role"] == "training" + assert labels["team"] == "rl" + assert labels["run-id"] == "r-1" + assert labels["app.kubernetes.io/managed-by"] == "nrl-k8s" def test_extra_labels_win_on_collision(self) -> None: cluster = _make_cluster(labels={"team": "cluster"}) @@ -133,11 +133,11 @@ def test_extra_labels_win_on_collision(self) -> None: ) assert got["metadata"]["labels"]["team"] == "extra" - def test_no_labels_key_when_empty(self) -> None: + def test_managed_by_label_always_present(self) -> None: got = build_rayjob_manifest( _make_cluster(), _make_infra(), entrypoint="x" ) - assert "labels" not in got["metadata"] + assert got["metadata"]["labels"]["app.kubernetes.io/managed-by"] == "nrl-k8s" class TestImmutability: From 2870dc7e410b917c0b0dce66bffef1df568ef8c3 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 01:53:34 -0700 Subject: [PATCH 47/84] feat(nrl-k8s): stale rayjob check, pod label propagation, README fix - Check all roles for existing RayJobs upfront before submitting; error with all delete commands at once (generic _find_stale_resources). - Propagate nrl-k8s/owner + managed-by labels to pod templates so kubectl get pods -l nrl-k8s/owner=$(whoami) finds RayJob pods. - Fix README: launch --replace -> run --raycluster --replace. Signed-off-by: Terry Kong --- tools/nrl_k8s/README.md | 2 +- tools/nrl_k8s/src/nrl_k8s/cli.py | 75 +++++++++++++++++++++++++++ tools/nrl_k8s/src/nrl_k8s/manifest.py | 15 ++++++ 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/tools/nrl_k8s/README.md b/tools/nrl_k8s/README.md index 4bfc577e601..d2a7713a97c 100644 --- a/tools/nrl_k8s/README.md +++ b/tools/nrl_k8s/README.md @@ -317,7 +317,7 @@ auto-managed. ### `nrl-k8s job stop --role ` Stop a Ray Job by submission id. Useful for clearing a stuck training job -before a re-run (though `launch --replace` does this automatically). +before a re-run (though `run --raycluster --replace` does this automatically). ### `nrl-k8s dev` diff --git a/tools/nrl_k8s/src/nrl_k8s/cli.py b/tools/nrl_k8s/src/nrl_k8s/cli.py index ba1eb35f419..fd432bb95c4 100644 --- a/tools/nrl_k8s/src/nrl_k8s/cli.py +++ b/tools/nrl_k8s/src/nrl_k8s/cli.py @@ -9,6 +9,7 @@ import json import sys +from dataclasses import dataclass from pathlib import Path from typing import NoReturn @@ -564,6 +565,8 @@ def _run_rayjob( if not submit_mod.is_in_cluster(): _preflight_or_exit(namespace) + _check_stale_rayjobs(loaded, namespace) + orchestrate.ensure_dra_resources("training", loaded, log=click.echo) click.echo(f"[run --rayjob] applying RayJob {job_name} in {namespace}") try: @@ -1319,6 +1322,78 @@ def _infer_namespace() -> str: # ============================================================================= +@dataclass +class _StaleResource: + kind: str # e.g. "rayjob", "raycluster", "pod" + name: str + status: str + + +def _find_stale_resources( + checks: list[tuple[str, str, callable]], +) -> list[_StaleResource]: + """Probe a list of ``(kind, name, getter)`` and return those that exist. + + ``getter(name)`` should return a status string if the resource exists, + or ``None`` if it doesn't. Generic so callers can check any resource type. + """ + stale: list[_StaleResource] = [] + for kind, name, getter in checks: + status = getter(name) + if status is not None: + stale.append(_StaleResource(kind=kind, name=name, status=status)) + return stale + + +def _error_on_stale(stale: list[_StaleResource], namespace: str) -> None: + """If ``stale`` is non-empty, print all resources and their delete commands, then exit.""" + if not stale: + return + + lines = ["stale resources from a previous run exist:\n"] + for r in stale: + lines.append(f" {r.kind}/{r.name} (status={r.status})") + lines.append("\ndelete them and resubmit:") + for r in stale: + lines.append(f" kubectl delete {r.kind} {r.name} -n {namespace}") + + _cli_error( + "\n".join(lines), + hint="once deleted, re-run the same nrl-k8s run command", + ) + + +def _check_stale_rayjobs(loaded: LoadedConfig, namespace: str) -> None: + """Check all roles for existing RayJobs upfront. + + Reports every stale RayJob at once so the user can clean up in one pass + rather than hitting them one at a time in a loop. + + Stale DRA resources (ComputeDomain, RoCE ResourceClaimTemplate) are not + checked — they are 1:1 with the RayJob by design (named after the + cluster + role), so deleting the RayJob and resubmitting will recreate + them idempotently. TODO: add DRA garbage collection to a future + ``nrl-k8s clean`` command. + """ + from . import k8s + from .orchestrate import ALL_ROLES, _get_cluster + + def _rayjob_status(name: str) -> str | None: + existing = k8s.get_rayjob(name, namespace) + if existing is None: + return None + return (existing.get("status") or {}).get("jobDeploymentStatus", "Pending") + + checks = [] + for role in ALL_ROLES: + cluster = _get_cluster(loaded.infra, role) + if cluster is None: + continue + checks.append(("rayjob", cluster.name, _rayjob_status)) + + _error_on_stale(_find_stale_resources(checks), namespace) + + def _preflight_or_exit(namespace: str) -> None: """Fail fast when kubectl is missing or RBAC is wrong — before we spawn anything.""" from . import submit diff --git a/tools/nrl_k8s/src/nrl_k8s/manifest.py b/tools/nrl_k8s/src/nrl_k8s/manifest.py index 0fe01b0deaa..739bd92396e 100644 --- a/tools/nrl_k8s/src/nrl_k8s/manifest.py +++ b/tools/nrl_k8s/src/nrl_k8s/manifest.py @@ -55,6 +55,7 @@ def build_raycluster_manifest( _patch_image_pull_secrets(spec, list(infra.imagePullSecrets)) if infra.serviceAccount is not None: _patch_service_account(spec, infra.serviceAccount) + _patch_pod_labels(spec, {**_MANAGED_BY_LABEL, **infra.labels, **cluster.labels}) # DRA resources are named {prefix}-{cluster_name}-{role} so that # disaggregated setups with multiple clusters get distinct # ComputeDomains and RoCE templates per role. @@ -99,6 +100,20 @@ def _walk_pod_templates(raycluster_spec: dict) -> list[dict]: return specs +def _patch_pod_labels(raycluster_spec: dict, labels: dict[str, str]) -> None: + """Merge ``labels`` into every pod template's metadata.labels.""" + head = raycluster_spec.get("headGroupSpec") or {} + templates = [head.get("template")] + for wg in raycluster_spec.get("workerGroupSpecs") or []: + templates.append(wg.get("template")) + for tpl in templates: + if not isinstance(tpl, dict): + continue + meta = tpl.setdefault("metadata", {}) + existing = meta.get("labels") or {} + meta["labels"] = {**labels, **existing} + + def _patch_images(raycluster_spec: dict, image: str) -> None: for pod_spec in _walk_pod_templates(raycluster_spec): for container in pod_spec.get("containers", []): From 5f2367bc8fc1f3c682544bad5e30815cba187605 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 01:53:50 -0700 Subject: [PATCH 48/84] feat(nrl-k8s): tee driver logs to workspace, simplify entrypoints - GB300 entrypoints check /mnt/rl-workspace exists, tee stdout/stderr to driver_logs/--.log on the shared PVC. - Drop RUN_ID variable; use static wandb.name for experiment identity. - Nanosecond timestamps to avoid log filename collisions. Signed-off-by: Terry Kong --- .../qwen3_30b_math_8n_4gpu.gb300.infra.yaml | 18 +++++++++++------- .../qwen3_4b_if_single.gb300.prod.infra.yaml | 10 +++++++++- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml index 1ad146dcb47..fac69041fe0 100644 --- a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml @@ -119,23 +119,27 @@ launch: # example don't apply here. # # Hydra overrides flip wandb on at runtime so the recipe on disk doesn't - # need to hardcode `logger.wandb_enabled=true`. RUN_ID is derived from the - # KubeRay submission id (RayJob mode) or NRL_K8S_RUN_ID (launch/run/go) - # so each submission gets a distinct wandb run name; falls back to a - # timestamp when neither is set. Backslashes escape OmegaConf - # interpolation so the `$VAR` text reaches the pod shell verbatim. + # need to hardcode `logger.wandb_enabled=true`. entrypoint: | set -eu cd /opt/nemo-rl + if [ ! -d /mnt/rl-workspace ]; then + echo "ERROR: /mnt/rl-workspace not mounted — is the rl-workspace PVC bound?" >&2 + exit 1 + fi export RAY_enable_infeasible_task_early_exit=true export PYTHONPATH=.:3rdparty/Megatron-LM-workspace/Megatron-LM - RUN_ID="\${RAY_JOB_SUBMISSION_ID:-\${NRL_K8S_RUN_ID:-$(date -u +%Y%m%d-%H%M%S)}}" + TIMESTAMP=$(date -u +%Y%m%d-%H%M%S-%N) + LOG_DIR=/mnt/rl-workspace/${user:}/driver_logs + LOG=\${LOG_DIR}/${user:}-raycluster-qwen3-30b-math-gb300-training-\${TIMESTAMP}.log + mkdir -p "\${LOG_DIR}" python -u examples/run_grpo.py \ --config tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml \ logger.wandb_enabled=true \ logger.wandb.project=nemorl-single-k8s \ - "logger.wandb.name=qwen3-30b-math-gb300-\${RUN_ID}" + logger.wandb.name=qwen3-30b-math-gb300 \ + 2>&1 | tee "\${LOG}" clusters: training: diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml index acae3b9f9c0..abb983c2b97 100644 --- a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml @@ -107,6 +107,10 @@ launch: entrypoint: | set -eu cd /opt/nemo-rl + if [ ! -d /mnt/rl-workspace ]; then + echo "ERROR: /mnt/rl-workspace not mounted — is the rl-workspace PVC bound?" >&2 + exit 1 + fi # Full instruction-following dataset (~20K prompts) snapshotted from # huggingface.co/datasets/nvidia/Nemotron-RL-instruction_following onto the # shared Lustre workspace. @@ -115,12 +119,16 @@ launch: export DISAGG_VALID_PATH=$IF_DATA export RAY_enable_infeasible_task_early_exit=true export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM + LOG_DIR=/mnt/rl-workspace/${user:}/driver_logs + LOG=\${LOG_DIR}/${user:}-raycluster-single-qwen3-4b-gb300-prod-training-$(date -u +%Y%m%d-%H%M%S-%N).log + mkdir -p "\${LOG_DIR}" python -u examples/nemo_gym/run_grpo_nemo_gym.py \ --config tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ ~policy.optimizer.kwargs.foreach \ - ~policy.optimizer.kwargs.fused + ~policy.optimizer.kwargs.fused \ + 2>&1 | tee "\${LOG}" clusters: training: From 012fbb0d46a8667f8eb9d46c14b5126a4c95ae88 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 16:19:19 -0700 Subject: [PATCH 49/84] feat(nrl-k8s): add --add-rclone to dev setup-secrets, install rclone in dev pod Signed-off-by: Terry Kong --- tools/nrl_k8s/src/nrl_k8s/cli.py | 21 +++++++++++++++++++-- tools/nrl_k8s/src/nrl_k8s/dev.py | 7 +++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/tools/nrl_k8s/src/nrl_k8s/cli.py b/tools/nrl_k8s/src/nrl_k8s/cli.py index fd432bb95c4..a8527d2e049 100644 --- a/tools/nrl_k8s/src/nrl_k8s/cli.py +++ b/tools/nrl_k8s/src/nrl_k8s/cli.py @@ -1253,9 +1253,17 @@ def dev_stop(namespace: str | None) -> None: multiple=True, help="Path to an SSH private key (repeatable).", ) +@click.option( + "--add-rclone", + is_flag=True, + help="Read ~/.config/rclone/rclone.conf and store it in the secret.", +) @click.option("--namespace", "-n", default=None, help="Kubernetes namespace.") def dev_setup_secrets( - kvs: tuple[str, ...], ssh_key: tuple[str, ...], namespace: str | None + kvs: tuple[str, ...], + ssh_key: tuple[str, ...], + add_rclone: bool, + namespace: str | None, ) -> None: """Create or update your user secrets. @@ -1266,7 +1274,7 @@ def dev_setup_secrets( \b nrl-k8s dev setup-secrets \\ HF_TOKEN=hf_xxx WANDB_API_KEY=key_yyy \\ - --ssh-key ~/.ssh/id_ed25519 + --ssh-key ~/.ssh/id_ed25519 --add-rclone Subsequent runs accept any subset to update individual keys. """ @@ -1292,6 +1300,15 @@ def dev_setup_secrets( data["SSH_KEY_NAME"] = p.name data["SSH_KEY_CONTENT"] = p.read_text() + if add_rclone: + rclone_conf = Path.home() / ".config" / "rclone" / "rclone.conf" + if not rclone_conf.exists(): + _cli_error( + f"rclone config not found at {rclone_conf}", + hint="install rclone and run `rclone config` first", + ) + data["RCLONE_CONF"] = rclone_conf.read_text() + is_new = not k8s.secret_exists(secret_name, namespace) if is_new: missing = [k for k in _REQUIRED_FIRST_TIME if k not in data] diff --git a/tools/nrl_k8s/src/nrl_k8s/dev.py b/tools/nrl_k8s/src/nrl_k8s/dev.py index c4dd18c634e..f9c557c0eb5 100644 --- a/tools/nrl_k8s/src/nrl_k8s/dev.py +++ b/tools/nrl_k8s/src/nrl_k8s/dev.py @@ -25,6 +25,13 @@ def build_dev_pod_manifest( 'printf "%s\\n" "$SSH_KEY_CONTENT" > /root/.ssh/$SSH_KEY_NAME && ' "chmod 600 /root/.ssh/$SSH_KEY_NAME; " "fi && " + 'if [ -n "$RCLONE_CONF" ]; then ' + "mkdir -p /root/.config/rclone && " + 'printf "%s\\n" "$RCLONE_CONF" > /root/.config/rclone/rclone.conf && ' + "if ! command -v rclone >/dev/null 2>&1; then " + "curl -sSf https://rclone.org/install.sh | bash; " + "fi; " + "fi && " "sleep infinity" ) From 0d3fdd7c10d001fd45cee5f331017af5edc8749b Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 22:24:31 -0700 Subject: [PATCH 50/84] chore: move qwen3_4b examples out of upstream scope Signed-off-by: Terry Kong --- .../qwen3_4b_if_full_disagg.infra.yaml | 386 ------------------ .../examples/qwen3_4b_if_full_disagg.yaml | 33 -- .../qwen3_4b_if_gym_disagg.infra.yaml | 222 ---------- .../qwen3_4b_if_gym_disagg.prod.infra.yaml | 244 ----------- .../examples/qwen3_4b_if_gym_disagg.yaml | 49 --- .../qwen3_4b_if_single.gb300.infra.yaml | 150 ------- .../qwen3_4b_if_single.gb300.prod.infra.yaml | 181 -------- .../examples/qwen3_4b_if_single.infra.yaml | 169 -------- .../nrl_k8s/examples/qwen3_4b_if_single.yaml | 50 --- 9 files changed, 1484 deletions(-) delete mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml delete mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml delete mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml delete mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml delete mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml delete mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml delete mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml delete mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml delete mode 100644 tools/nrl_k8s/examples/qwen3_4b_if_single.yaml diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml deleted file mode 100644 index c9dd942b71f..00000000000 --- a/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml +++ /dev/null @@ -1,386 +0,0 @@ -# Infra for the Qwen3-4B full-disaggregated run — paired with qwen3_4b_if_full_disagg.yaml. -# -# Usage: -# nrl-k8s run tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml -# -# Everything K8s-specific lives here so the recipe stays cluster-agnostic. -# Shared YAML anchors below keep the three cluster specs from repeating. - -# ============================================================================= -# Shared anchors — consumed during YAML parse. -# ============================================================================= -_shared: - nodeSelector: &shared_node_selector - gpu-wrangler.nvidia.com/lease: nemo-rl-testing - tolerations: &shared_tolerations - - {key: gpu-wrangler.nvidia.com/lease, operator: Equal, value: nemo-rl-testing, effect: NoSchedule} - - {key: platform.nvidia.com/gpu, operator: Equal, value: "true", effect: NoSchedule} - # Keep pod-spec env minimal — the nemo-rl:nightly image already sets - # NCCL_NET_PLUGIN=aws-ofi, NCCL_SOCKET_IFNAME=enp71s0, etc. in its shell - # init. Overriding them here just causes Ray runtime-env merge conflicts - # when ray.init() captures os.environ inside the job entrypoint. - headEnv: &shared_head_env - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - workerEnv: &shared_worker_env - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - headResources: &shared_head_resources - limits: {cpu: "8", memory: "32Gi"} - requests: {cpu: "2", memory: "8Gi"} - gpuWorkerResources: &gpu_worker_resources - # Sized for p5.48xlarge (192 CPU / ~1957 GiB allocatable). We claim - # most of the node and leave ~16 CPU + ~150 GiB headroom for: - # - the training head pod colocated here (~8 CPU / 32 GiB) - # - a gym head pod if it lands on this node (~8 CPU / 32 GiB) - # - daemonsets: kube-proxy, aws-node, nvidia-device-plugin, - # node-exporter (~4 CPU / 16 GiB total) - # p5.48xlarge also exposes 32 EFA-capable ENIs. The AWS OFI NCCL plugin - # (bundled in the image as NCCL_NET_PLUGIN=aws-ofi) needs them mapped - # into the container as `vpc.amazonaws.com/efa` resources — the EFA - # device plugin on the node injects the character devices under - # /dev/infiniband/ and the libfabric provider discovers them at - # startup. Drop the EFA request and you'll see `FI_PROVIDER=efa` log - # "No usable providers found" and NCCL fall back to socket / hang. - # Also: `nvidia.com/gpu: "8"` must equal the rayStartParams num-gpus - # on the same worker template, otherwise Ray's resource view disagrees - # with what CUDA actually sees. - limits: - cpu: "176" - memory: "1800Gi" - nvidia.com/gpu: "8" - vpc.amazonaws.com/efa: "32" - requests: - cpu: "176" - memory: "1800Gi" - nvidia.com/gpu: "8" - vpc.amazonaws.com/efa: "32" - gpuHeadPorts: &gpu_head_ports - - {containerPort: 6379, name: gcs-server} - - {containerPort: 8265, name: dashboard} - - {containerPort: 10001, name: client} - dshmMounts: &dshm_mounts - - {mountPath: /dev/shm, name: dshm} - dshm16: &dshm_16 - - name: dshm - emptyDir: {medium: Memory, sizeLimit: 16Gi} - dshm64: &dshm_64 - - name: dshm - emptyDir: {medium: Memory, sizeLimit: 64Gi} - dshm4: &dshm_4 - - name: dshm - emptyDir: {medium: Memory, sizeLimit: 4Gi} - -# ============================================================================= -# Top-level InfraConfig (no wrapping `infra:` needed — the CLI accepts either) -# ============================================================================= -namespace: nemo-rl-testing -image: nvcr.io/nvidian/nemo-rl:nightly -imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] -serviceAccount: nemo-rl-endpoint-registry - -labels: - nrl-k8s/owner: ${user:} - -launch: - mode: attach - attach: - generation: ${user:}-raycluster-generation-qwen3-4b - gym: ${user:}-raycluster-gym-qwen3-4b - training: ${user:}-raycluster-rl-qwen3-4b - peerWatcher: false - # Minimal upload set — Ray caps working_dir at 100 MiB. We include only - # what this run needs: nemo_rl, examples, the nemo_gym subset, and the - # instruction_following data (~90 MiB, fits under the cap). - rayUploadPaths: - - nemo_rl - - examples - - infra/examples - - tests/check_metrics.py - - tests/json_dump_tb_logs.py - - 3rdparty/Gym-workspace/Gym/nemo_gym - - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model - - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent - - 3rdparty/Gym-workspace/Gym/pyproject.toml - - 3rdparty/Gym-workspace/Gym/uv.lock - # Only configs (not data/*.jsonl) — the big train/validation jsonls are - # pre-staged via kubectl cp onto the training pods' /tmp (see env above). - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs - # nemo_rl.distributed.model_utils imports megatron.core. - - 3rdparty/Megatron-LM-workspace/Megatron-LM/megatron - # The CLI stages the merged recipe as ./nrl_k8s_run.yaml at the working_dir - # root — reference it by name from the entrypoint. - entrypoint: | - # kubernetes client is required by K8sEndpointRegistry (used by training - # to read vllm_base_urls from the ConfigMap). The image *usually* has it, - # but installing here is cheap insurance and keeps the recipe portable. - pip install kubernetes -q || true - # Inline exports — see note on launch.env below. - # GLOO_SOCKET_IFNAME: PyTorch distributed gloo backend (used for the - # rendezvous store). Default eth0 isn't the interface on p5 hosts. - export GLOO_SOCKET_IFNAME=enp71s0 - # NCCL_SOCKET_IFNAME: control-plane interface NCCL uses before OFI - # takes over data. Must match the host's primary AWS VPC ENI. - export NCCL_SOCKET_IFNAME=enp71s0 - # FI_PROVIDER: tells libfabric (and thus aws-ofi-nccl) to use EFA. - # Without it you'd get the TCP provider over eth0, ~10x slower. - export FI_PROVIDER=efa - # Data paths — files were pre-staged on the pods via kubectl cp onto - # each training pod's /tmp (the 87 MB jsonl blows the 100 MiB - # working_dir cap, so Ray can't ship it). - export DISAGG_TRAIN_PATH=/tmp/train.jsonl - export DISAGG_VALID_PATH=/tmp/validation.jsonl - # expandable_segments reduces CUDA memory fragmentation under varying - # batch shapes. Safe to set in disagg mode; UNSAFE in colocated mode - # (vLLM's CuMemAllocator asserts). The single-cluster recipe omits it. - export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True - # Kill Ray tasks that can never schedule instead of letting them - # queue forever — surfaces cluster-capacity bugs fast. - export RAY_enable_infeasible_task_early_exit=true - # Make sibling workspaces importable alongside the repo root. - export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM - # disagg_job_id is passed in three places — here (training), - # the gen daemon export DISAGG_JOB_ID (generation), and the gym daemon's - # --job-id flag (gym). It is deliberately NOT an InfraConfig field: the - # CLI infers the ConfigMap name by regex-parsing --job-id out of the gym - # entrypoint (orchestrate.py:_infer_disagg_job_id), so adding a separate - # key would just duplicate information and let the two go out of sync. - # Keep all three spellings in sync when you rename a run. - python -u examples/nemo_gym/run_grpo_nemo_gym.py \ - --config nrl_k8s_run.yaml \ - +policy.generation.remote_generation_url=http://raycluster-generation-qwen3-4b-head-svc.nemo-rl-testing.svc.cluster.local:8089 \ - +env.disagg_job_id=qwen3-4b-if-gym \ - ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ - ~policy.optimizer.kwargs.foreach \ - ~policy.optimizer.kwargs.fused - # All env flows through inline `export`s in the entrypoint — setting - # anything here puts it in runtime_env.env_vars, which can't merge with - # ray.init's captured env_vars and aborts with "Failed to merge". - env: {} - -clusters: - # ------------------------------------------------------------------------- - # Generation — GPU head + worker, control-server on 8089. - # ------------------------------------------------------------------------- - generation: - name: ${user:}-raycluster-generation-qwen3-4b - labels: - disagg.nemo-rl/cluster: generation-qwen3-4b - disagg.nemo-rl/run: qwen3-4b-gym - daemon: - submissionId: qwen3-4b-generation-server-v7 - # Inline exports (not runtime_env.env_vars, which conflicts with - # ray.init's captured env). The Python process calls ray.init() after - # these are exported, ray captures them into its env, and propagates - # them to the worker actors + their subprocesses — so vLLM's - # EngineCore sees GLOO_SOCKET_IFNAME=enp71s0 and doesn't try eth0. - # DISAGG_JOB_ID triggers the K8sEndpointRegistry publish path so - # gym can discover the gen shards via the ConfigMap. - entrypoint: | - export GLOO_SOCKET_IFNAME=enp71s0 - export NCCL_SOCKET_IFNAME=enp71s0 - export FI_PROVIDER=efa - export DISAGG_JOB_ID=qwen3-4b-if-gym - # K8sEndpointRegistry needs the `kubernetes` client. - pip install kubernetes -q || true - python -u examples/run_standalone_generation_server.py \ - --config infra/examples/generation_standalone_qwen3_4b_dapo.yaml \ - --port 8089 --num-gpus 8 - # Gen server doesn't need training data — keep the upload small. - rayUploadPaths: - - nemo_rl - - examples - - infra/examples - spec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - dnsPolicy: ClusterFirst - nodeSelector: *shared_node_selector - tolerations: *shared_tolerations - containers: - - name: ray-head - env: *shared_head_env - resources: *shared_head_resources - ports: - - {containerPort: 6379, name: gcs-server} - - {containerPort: 8265, name: dashboard} - - {containerPort: 10001, name: client} - - {containerPort: 8089, name: control-server} - volumeMounts: *dshm_mounts - volumes: *dshm_16 - workerGroupSpecs: - - groupName: gpu-workers - replicas: 1 - minReplicas: 1 - maxReplicas: 1 - rayStartParams: {num-gpus: "8", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - # hostNetwork=true on GPU workers is required for EFA: the - # AWS OFI NCCL plugin opens raw libfabric endpoints bound to - # the host ENI (enp71s0, above), which a pod network - # namespace can't see. With the default CNI network the - # plugin initialises but communication never completes and - # NCCL hangs at the first AllReduce. `dnsPolicy: - # ClusterFirstWithHostNet` then restores in-cluster DNS so - # the worker can still resolve the head svc and the gen - # server's FQDN. - hostNetwork: true - dnsPolicy: ClusterFirstWithHostNet - nodeSelector: *shared_node_selector - tolerations: *shared_tolerations - containers: - - name: ray-worker - env: *shared_worker_env - resources: *gpu_worker_resources - volumeMounts: *dshm_mounts - volumes: *dshm_64 - - # ------------------------------------------------------------------------- - # Gym — CPU-only head, gym-server on 9090. - # ------------------------------------------------------------------------- - gym: - name: ${user:}-raycluster-gym-qwen3-4b - daemon: - submissionId: qwen3-4b-if-gym-server-v7 - # Ray runs entrypoints under /bin/dash. We stay POSIX here (no - # pipefail, no process substitution). OmegaConf ${...} escaped as - # \${...} — Python dollar-refs for ray.__version__ use a one-liner. - entrypoint: | - set -eu - ROOT_DIR=$(pwd) - GYM_DIR=\${ROOT_DIR}/3rdparty/Gym-workspace/Gym - export PYTHONPATH=\${ROOT_DIR}:\${GYM_DIR}:\${PYTHONPATH-} - cd \${GYM_DIR} - mkdir -p cache - RAY_VERSION=$(python -c "import ray; print(ray.__version__)") - echo "ray==\${RAY_VERSION}" > /tmp/ray-pin.txt - pip install -e "." --constraint /tmp/ray-pin.txt -q - pip install kubernetes -q - exec python -m nemo_gym.standalone_server \ - --job-id qwen3-4b-if-gym \ - --port 9090 \ - --model-name Qwen/Qwen3-4B-Instruct-2507 \ - --config-yaml \${ROOT_DIR}/infra/examples/gym_standalone_config_instruction_following.yaml - # Gym needs Gym code + infra configs + the nemo_rl.distributed package - # for the k8s endpoint registry. We list instruction_following files - # individually instead of the whole resources_servers/instruction_following - # directory because that directory holds an 87 MB train.jsonl — Ray's - # 100 MiB working_dir cap would kick in and submission would fail. - # Listing app.py and requirements.txt by name picks up the FastAPI - # resource server and the rubric-grader wheels at daemon start - # (the `pip install -e "."` above reads them) without shipping the - # bulk dataset. The smaller validation.jsonl / example.jsonl / etc. - # are included because gym's smoke tests and rollout warm-up read - # them, and together they're under 2 MiB. - rayUploadPaths: - - nemo_rl - - infra/examples - - 3rdparty/Gym-workspace/Gym/nemo_gym - - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model - - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent - - 3rdparty/Gym-workspace/Gym/pyproject.toml - - 3rdparty/Gym-workspace/Gym/uv.lock - # app.py: the instruction_following FastAPI resource server entry. - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/app.py - # requirements.txt: rubric-grader deps gym installs on daemon start. - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/requirements.txt - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/tests - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/validation.jsonl - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example.jsonl - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example_rollouts.jsonl - spec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: {num-gpus: "0", dashboard-host: "0.0.0.0", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - dnsPolicy: ClusterFirst - nodeSelector: *shared_node_selector - tolerations: *shared_tolerations - containers: - - name: ray-head - env: - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - resources: *shared_head_resources - ports: - - {containerPort: 6379, name: gcs-server} - - {containerPort: 8265, name: dashboard} - - {containerPort: 10001, name: client} - - {containerPort: 9090, name: gym-server} - volumeMounts: *dshm_mounts - volumes: *dshm_4 - - # ------------------------------------------------------------------------- - # Training — GPU head + worker. Head has WANDB secret. - # ------------------------------------------------------------------------- - training: - name: ${user:}-raycluster-rl-qwen3-4b - labels: - disagg.nemo-rl/cluster: rl-qwen3-4b - disagg.nemo-rl/run: qwen3-4b-gym - # No daemon: waits for `nrl-k8s run` / `nrl-k8s rayjob`. - spec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - dnsPolicy: ClusterFirst - nodeSelector: *shared_node_selector - tolerations: *shared_tolerations - containers: - - name: ray-head - env: - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: GLOO_SOCKET_IFNAME, value: eth0} - - {name: NCCL_SOCKET_IFNAME, value: eth0} - - {name: NCCL_DEBUG, value: INFO} - - {name: NCCL_IB_DISABLE, value: "1"} - - {name: NCCL_NET, value: Socket} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - - name: WANDB_API_KEY - valueFrom: - secretKeyRef: - name: wandb-api-key - key: WANDB_API_KEY - resources: *shared_head_resources - ports: *gpu_head_ports - volumeMounts: *dshm_mounts - volumes: *dshm_16 - workerGroupSpecs: - - groupName: gpu-workers - replicas: 1 - minReplicas: 1 - maxReplicas: 1 - rayStartParams: {num-gpus: "8", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - # Same EFA + NCCL reasoning as the generation worker above. - # Training AllReduces run across these workers via aws-ofi; - # without hostNetwork they hang at the first collective. - hostNetwork: true - dnsPolicy: ClusterFirstWithHostNet - nodeSelector: *shared_node_selector - tolerations: *shared_tolerations - containers: - - name: ray-worker - env: *shared_worker_env - resources: *gpu_worker_resources - volumeMounts: *dshm_mounts - volumes: *dshm_64 diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml deleted file mode 100644 index 8c7d1b545f6..00000000000 --- a/tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# Recipe: Qwen3-4B disaggregated GRPO on instruction_following gym. -# -# Pure NeMo-RL recipe — no infra. Runs in any environment. Pair with an infra -# file via ``nrl-k8s run qwen3_4b_if.yaml --infra qwen3_4b_if.infra.yaml``. -defaults: ../../../examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml - -cluster: - gpus_per_node: 8 - num_nodes: 1 -grpo: - num_prompts_per_step: 32 - num_generations_per_prompt: 16 - max_num_steps: 200 - val_period: 50 - max_rollout_turns: 1 - async_grpo: - max_trajectory_age_steps: 1 -policy: - train_global_batch_size: 512 - train_micro_batch_size: 1 - logprob_batch_size: 1 - max_total_sequence_length: 16384 - dtensor_cfg: - activation_checkpointing: true -checkpointing: - save_period: 50 -logger: - wandb_enabled: true - wandb: - project: nemorl-full-disagg-k8s - name: full-disagg-if-qwen3-4b - tensorboard_enabled: true - monitor_gpus: true diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml deleted file mode 100644 index 3794350e956..00000000000 --- a/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml +++ /dev/null @@ -1,222 +0,0 @@ -# Infra for gym-disaggregated Qwen3-4B colocated GRPO run. -# -# Two RayClusters: a single GPU cluster for training (with generation -# colocated inside it) and the usual CPU-only gym cluster. No separate -# generation cluster. - -_shared: - nodeSelector: &shared_node_selector - gpu-wrangler.nvidia.com/lease: nemo-rl-testing - tolerations: &shared_tolerations - - {key: gpu-wrangler.nvidia.com/lease, operator: Equal, value: nemo-rl-testing, effect: NoSchedule} - - {key: platform.nvidia.com/gpu, operator: Equal, value: "true", effect: NoSchedule} - # Image ships aws-ofi + enp71s0 defaults; keep pod env minimal. - headEnv: &shared_head_env - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - workerEnv: &shared_worker_env - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - headResources: &shared_head_resources - limits: {cpu: "8", memory: "32Gi"} - requests: {cpu: "2", memory: "8Gi"} - gpuWorkerResources: &gpu_worker_resources - # Sized for p5.48xlarge (192 CPU / ~1957 GiB allocatable). Claim most - # of the node; leave ~16 CPU + ~150 GiB for the training head, a - # colocated gym head, and k8s daemonsets (kube-proxy, aws-node, - # nvidia-device-plugin, node-exporter). - limits: - cpu: "176" - memory: "1800Gi" - nvidia.com/gpu: "8" - vpc.amazonaws.com/efa: "32" - requests: - cpu: "176" - memory: "1800Gi" - nvidia.com/gpu: "8" - vpc.amazonaws.com/efa: "32" - gpuHeadPorts: &gpu_head_ports - - {containerPort: 6379, name: gcs-server} - - {containerPort: 8265, name: dashboard} - - {containerPort: 10001, name: client} - dshmMounts: &dshm_mounts - - {mountPath: /dev/shm, name: dshm} - dshm16: &dshm_16 - - name: dshm - emptyDir: {medium: Memory, sizeLimit: 16Gi} - dshm64: &dshm_64 - - name: dshm - emptyDir: {medium: Memory, sizeLimit: 64Gi} - dshm4: &dshm_4 - - name: dshm - emptyDir: {medium: Memory, sizeLimit: 4Gi} - -namespace: nemo-rl-testing -image: nvcr.io/nvidian/nemo-rl:nightly -imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] -serviceAccount: nemo-rl-endpoint-registry - -labels: - nrl-k8s/owner: ${user:} - -launch: - mode: attach - attach: - gym: ${user:}-raycluster-gym-disagg-gym-qwen3-4b - training: ${user:}-raycluster-gym-disagg-qwen3-4b - peerWatcher: false - # Colocated generation → no --remote_generation_url override. - # All env flows through inline exports (runtime_env.env_vars merge-fails - # when ray.init also has env_vars). - entrypoint: | - pip install kubernetes -q || true - export GLOO_SOCKET_IFNAME=enp71s0 - export NCCL_SOCKET_IFNAME=enp71s0 - export FI_PROVIDER=efa - # Data files are pre-staged on the pods via kubectl cp. - export DISAGG_TRAIN_PATH=/tmp/train.jsonl - export DISAGG_VALID_PATH=/tmp/validation.jsonl - # NOTE: PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True is incompatible - # with colocated vLLM's CuMemAllocator (AssertionError at vLLM init). - # Disagg sets it because gen doesn't use the memory pool; single does. - export RAY_enable_infeasible_task_early_exit=true - export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM - python -u examples/nemo_gym/run_grpo_nemo_gym.py \ - --config nrl_k8s_run.yaml \ - +env.disagg_job_id=qwen3-4b-gym-disagg \ - ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ - ~policy.optimizer.kwargs.foreach \ - ~policy.optimizer.kwargs.fused - env: {} - rayUploadPaths: - - nemo_rl - - examples - - infra/examples - - tests/check_metrics.py - - tests/json_dump_tb_logs.py - - 3rdparty/Gym-workspace/Gym/nemo_gym - - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model - - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent - - 3rdparty/Gym-workspace/Gym/pyproject.toml - - 3rdparty/Gym-workspace/Gym/uv.lock - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs - - 3rdparty/Megatron-LM-workspace/Megatron-LM/megatron - -clusters: - # ---------------- Gym (CPU, head-only) ---------------- - gym: - name: ${user:}-raycluster-gym-disagg-gym-qwen3-4b - daemon: - submissionId: qwen3-4b-gym-disagg-server-v5 - entrypoint: | - set -eu - ROOT_DIR=$(pwd) - GYM_DIR=\${ROOT_DIR}/3rdparty/Gym-workspace/Gym - export PYTHONPATH=\${ROOT_DIR}:\${GYM_DIR}:\${PYTHONPATH-} - cd \${GYM_DIR} - mkdir -p cache - RAY_VERSION=$(python -c "import ray; print(ray.__version__)") - echo "ray==\${RAY_VERSION}" > /tmp/ray-pin.txt - pip install -e "." --constraint /tmp/ray-pin.txt -q - pip install kubernetes -q - # In single-cluster mode training (run_grpo_nemo_gym.py) publishes - # vllm_base_urls to the endpoint registry once colocated vLLM spawns. - # Gym blocks on that key until training publishes — a two-way rendezvous. - exec python -m nemo_gym.standalone_server \ - --job-id qwen3-4b-gym-disagg \ - --port 9090 \ - --model-name Qwen/Qwen3-4B-Instruct-2507 \ - --config-yaml \${ROOT_DIR}/infra/examples/gym_standalone_config_instruction_following.yaml - rayUploadPaths: - - nemo_rl - - infra/examples - - 3rdparty/Gym-workspace/Gym/nemo_gym - - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model - - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent - - 3rdparty/Gym-workspace/Gym/pyproject.toml - - 3rdparty/Gym-workspace/Gym/uv.lock - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/app.py - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/requirements.txt - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/tests - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/validation.jsonl - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example.jsonl - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example_rollouts.jsonl - spec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: {num-gpus: "0", dashboard-host: "0.0.0.0", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - dnsPolicy: ClusterFirst - nodeSelector: *shared_node_selector - tolerations: *shared_tolerations - containers: - - name: ray-head - env: - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - resources: *shared_head_resources - ports: - - {containerPort: 6379, name: gcs-server} - - {containerPort: 8265, name: dashboard} - - {containerPort: 10001, name: client} - - {containerPort: 9090, name: gym-server} - volumeMounts: *dshm_mounts - volumes: *dshm_4 - - # ---------------- Training (GPU head + worker, generation colocated) ---------------- - training: - name: ${user:}-raycluster-gym-disagg-qwen3-4b - labels: - disagg.nemo-rl/cluster: gym-disagg-qwen3-4b - disagg.nemo-rl/run: qwen3-4b-gym-disagg - spec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - dnsPolicy: ClusterFirst - nodeSelector: *shared_node_selector - tolerations: *shared_tolerations - containers: - - name: ray-head - env: - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - - name: WANDB_API_KEY - valueFrom: - secretKeyRef: - name: wandb-api-key - key: WANDB_API_KEY - resources: *shared_head_resources - ports: *gpu_head_ports - volumeMounts: *dshm_mounts - volumes: *dshm_16 - workerGroupSpecs: - - groupName: gpu-workers - replicas: 1 - minReplicas: 1 - maxReplicas: 1 - rayStartParams: {num-gpus: "8", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - hostNetwork: true - dnsPolicy: ClusterFirstWithHostNet - nodeSelector: *shared_node_selector - tolerations: *shared_tolerations - containers: - - name: ray-worker - env: *shared_worker_env - resources: *gpu_worker_resources - volumeMounts: *dshm_mounts - volumes: *dshm_64 diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml deleted file mode 100644 index 86adb1b23a9..00000000000 --- a/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml +++ /dev/null @@ -1,244 +0,0 @@ -# Production-batch variant of qwen3_4b_if_gym_disagg.infra.yaml. -# -# Same recipe, same two RayClusters (training + gym), same runtime -# behaviour — but submission goes through `kubectl exec` into the -# training head pod and the code lives at /opt/nemo-rl inside the -# image. No working_dir upload; the submitting laptop can disconnect -# as soon as nohup fires. -# -# Usage: -# # Admins, once: bring up the two RayClusters. -# nrl-k8s cluster up tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ -# --role gym -# nrl-k8s cluster up tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ -# --role training -# -# # Researcher, per run — idempotent; reuses live clusters if matching, -# # submits training against the training cluster. Returns in seconds, -# # laptop disconnectable. Use --skip-daemons after first bring-up if -# # gym/generation are already healthy. -# nrl-k8s run tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ -# --run-id qwen3-4b-gym-disagg-$(date +%Y%m%d-%H%M%S) -# -# # Observe from anywhere; handle is cached on disk. -# nrl-k8s job logs \ -# tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ -# --role training -f - -_shared: - nodeSelector: &shared_node_selector - gpu-wrangler.nvidia.com/lease: nemo-rl-testing - tolerations: &shared_tolerations - - {key: gpu-wrangler.nvidia.com/lease, operator: Equal, value: nemo-rl-testing, effect: NoSchedule} - - {key: platform.nvidia.com/gpu, operator: Equal, value: "true", effect: NoSchedule} - headEnv: &shared_head_env - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - workerEnv: &shared_worker_env - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - headResources: &shared_head_resources - limits: {cpu: "8", memory: "32Gi"} - requests: {cpu: "2", memory: "8Gi"} - gpuWorkerResources: &gpu_worker_resources - limits: - cpu: "176" - memory: "1800Gi" - nvidia.com/gpu: "8" - vpc.amazonaws.com/efa: "32" - requests: - cpu: "176" - memory: "1800Gi" - nvidia.com/gpu: "8" - vpc.amazonaws.com/efa: "32" - gpuHeadPorts: &gpu_head_ports - - {containerPort: 6379, name: gcs-server} - - {containerPort: 8265, name: dashboard} - - {containerPort: 10001, name: client} - dshmMounts: &dshm_mounts - - {mountPath: /dev/shm, name: dshm} - dshm16: &dshm_16 - - name: dshm - emptyDir: {medium: Memory, sizeLimit: 16Gi} - dshm64: &dshm_64 - - name: dshm - emptyDir: {medium: Memory, sizeLimit: 64Gi} - dshm4: &dshm_4 - - name: dshm - emptyDir: {medium: Memory, sizeLimit: 4Gi} - -namespace: nemo-rl-testing -image: nvcr.io/nvidian/nemo-rl:nightly -imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] -serviceAccount: nemo-rl-endpoint-registry - -labels: - nrl-k8s/owner: ${user:} - -submit: - # Shell into the training head pod instead of port-forwarding the - # dashboard to the laptop. Once `nohup` + `disown` fire the laptop is - # off the critical path. - submitter: exec - execTmpDir: /tmp - -launch: - mode: attach - # runMode: batch flips defaults without --mode on the CLI. - runMode: batch - # Code is baked into the container at /opt/nemo-rl (the - # nvcr.io/nvidian/nemo-rl:nightly build ships it). No working_dir - # upload, no .gitignore strip, no 100 MiB cap. - codeSource: image - codePath: /opt/nemo-rl - attach: - gym: ${user:}-raycluster-gym-disagg-gym-qwen3-4b - training: ${user:}-raycluster-gym-disagg-qwen3-4b - peerWatcher: false - env: {} - # Entrypoint runs inside the training head pod under `nohup bash`. - # Env vars here propagate to Ray's worker actors via the standard - # os.environ capture — no runtime_env.env_vars gymnastics. - entrypoint: | - # Keep POSIX-compatible — Ray's port-forward path submits entrypoints - # through /bin/dash, which doesn't support `set -o pipefail`. - set -eu - cd /opt/nemo-rl - export GLOO_SOCKET_IFNAME=enp71s0 - export NCCL_SOCKET_IFNAME=enp71s0 - export FI_PROVIDER=efa - export DISAGG_TRAIN_PATH=/tmp/train.jsonl - export DISAGG_VALID_PATH=/tmp/validation.jsonl - export RAY_enable_infeasible_task_early_exit=true - export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM - - python -u examples/nemo_gym/run_grpo_nemo_gym.py \ - --config examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml \ - +env.disagg_job_id=qwen3-4b-gym-disagg \ - ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ - ~policy.optimizer.kwargs.foreach \ - ~policy.optimizer.kwargs.fused - -clusters: - # ---------------- Gym (CPU, head-only) ---------------- - # Gym daemon still submits via port-forward + working_dir upload — - # it's a long-lived server admins bring up once, so the upload cost - # is paid once per `cluster up` and the researcher's `launch` never - # touches it. - gym: - name: ${user:}-raycluster-gym-disagg-gym-qwen3-4b - daemon: - submissionId: qwen3-4b-gym-disagg-server-v5 - entrypoint: | - set -eu - ROOT_DIR=$(pwd) - GYM_DIR=\${ROOT_DIR}/3rdparty/Gym-workspace/Gym - export PYTHONPATH=\${ROOT_DIR}:\${GYM_DIR}:\${PYTHONPATH-} - cd \${GYM_DIR} - mkdir -p cache - RAY_VERSION=$(python -c "import ray; print(ray.__version__)") - echo "ray==\${RAY_VERSION}" > /tmp/ray-pin.txt - pip install -e "." --constraint /tmp/ray-pin.txt -q - pip install kubernetes -q - exec python -m nemo_gym.standalone_server \ - --job-id qwen3-4b-gym-disagg \ - --port 9090 \ - --model-name Qwen/Qwen3-4B-Instruct-2507 \ - --config-yaml \${ROOT_DIR}/infra/examples/gym_standalone_config_instruction_following.yaml - rayUploadPaths: - - nemo_rl - - infra/examples - - 3rdparty/Gym-workspace/Gym/nemo_gym - - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model - - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent - - 3rdparty/Gym-workspace/Gym/pyproject.toml - - 3rdparty/Gym-workspace/Gym/uv.lock - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/app.py - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/requirements.txt - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/tests - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/validation.jsonl - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example.jsonl - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example_rollouts.jsonl - spec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: {num-gpus: "0", dashboard-host: "0.0.0.0", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - dnsPolicy: ClusterFirst - nodeSelector: *shared_node_selector - tolerations: *shared_tolerations - containers: - - name: ray-head - env: - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - resources: *shared_head_resources - ports: - - {containerPort: 6379, name: gcs-server} - - {containerPort: 8265, name: dashboard} - - {containerPort: 10001, name: client} - - {containerPort: 9090, name: gym-server} - volumeMounts: *dshm_mounts - volumes: *dshm_4 - - # ---------------- Training (GPU head + worker, generation colocated) ---------------- - training: - name: ${user:}-raycluster-gym-disagg-qwen3-4b - labels: - disagg.nemo-rl/cluster: gym-disagg-qwen3-4b - disagg.nemo-rl/run: qwen3-4b-gym-disagg - spec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - dnsPolicy: ClusterFirst - nodeSelector: *shared_node_selector - tolerations: *shared_tolerations - containers: - - name: ray-head - env: - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - - name: WANDB_API_KEY - valueFrom: - secretKeyRef: - name: wandb-api-key - key: WANDB_API_KEY - resources: *shared_head_resources - ports: *gpu_head_ports - volumeMounts: *dshm_mounts - volumes: *dshm_16 - workerGroupSpecs: - - groupName: gpu-workers - replicas: 1 - minReplicas: 1 - maxReplicas: 1 - rayStartParams: {num-gpus: "8", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - hostNetwork: true - dnsPolicy: ClusterFirstWithHostNet - nodeSelector: *shared_node_selector - tolerations: *shared_tolerations - containers: - - name: ray-worker - env: *shared_worker_env - resources: *gpu_worker_resources - volumeMounts: *dshm_mounts - volumes: *dshm_64 diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml deleted file mode 100644 index ad15c927faa..00000000000 --- a/tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml +++ /dev/null @@ -1,49 +0,0 @@ -# Qwen3-4B GRPO on instruction_following gym — **gym-disaggregated variant**. -# -# Differs from qwen3_4b_if.yaml in a single field: generation is colocated -# with training (both run inside the same Ray cluster's GPU workers), so we -# don't need a standalone generation RayCluster or its daemon. -# -# Pair with qwen3_4b_if_gym_disagg.infra.yaml (which declares only training + -# gym clusters — no generation cluster). -defaults: ../../../examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml - -cluster: - gpus_per_node: 8 - num_nodes: 1 -grpo: - num_prompts_per_step: 32 - num_generations_per_prompt: 16 - max_num_steps: 200 - val_period: 50 - max_rollout_turns: 1 - async_grpo: - # NeMo-RL asserts non-colocated when async_grpo is enabled. Single-cluster - # runs colocated generation, so we keep GRPO synchronous here. - enabled: false -policy: - train_global_batch_size: 256 - train_micro_batch_size: 1 - logprob_batch_size: 1 - # Colocated vLLM + backward on 4B takes too much GPU at 16K context; - # halving leaves headroom for vLLM's 6 GiB + activations + optimizer. - max_total_sequence_length: 8192 - dtensor_cfg: - activation_checkpointing: true - generation: - colocated: - enabled: true # recipe-level difference from disagg - resources: - gpus_per_node: 8 - num_nodes: 1 - vllm_cfg: - gpu_memory_utilization: 0.45 # leave 55% for training state -checkpointing: - save_period: 50 -logger: - wandb_enabled: true - wandb: - project: nemorl-gym-disagg-k8s - name: gym-disagg-if-qwen3-4b - tensorboard_enabled: true - monitor_gpus: true diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml deleted file mode 100644 index 7f23fabe063..00000000000 --- a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml +++ /dev/null @@ -1,150 +0,0 @@ -# Infra for qwen3_4b_if_single.yaml on a GB300 (p6e-gb300r.36xlarge) EKS cluster. -# -# Per-node hardware: 4× NVIDIA-GB300, ~140 CPU, ~925 GiB memory, arm64. GB300 -# nodes advertise no taints on this cluster — plain nodeSelector is enough. -# There is no aws-efa-k8s-device-plugin here, so vpc.amazonaws.com/efa is not -# a schedulable resource; NCCL/UCX ride the Mellanox NICs over hostNetwork. -# -# Sizing mirrors the paired recipe (1 node × 4 GPUs). Namespace is inferred -# from the current kube context (default — the FSx Lustre PVC `rl-workspace` -# is namespace-scoped and lives there, so experiments run in `default`). - -_shared: - nodeSelector: &shared_node_selector - nvidia.com/gpu.product: NVIDIA-GB300 - headEnv: &shared_head_env - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - workerEnv: &shared_worker_env - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - headResources: &shared_head_resources - limits: {cpu: "8", memory: "32Gi"} - requests: {cpu: "2", memory: "8Gi"} - gpuWorkerResources: &gpu_worker_resources - # p6e-gb300r.36xlarge allocatable: ~139.6 CPU, ~924 GiB, 4 GPUs. - # Claim 128 CPU / 880 GiB to leave headroom for the Ray head pod + - # gpu-operator / device-plugin / dgxc daemonsets (~11 CPU, ~40 GiB). - limits: - cpu: "128" - memory: "880Gi" - nvidia.com/gpu: "4" - requests: - cpu: "128" - memory: "880Gi" - nvidia.com/gpu: "4" - gpuHeadPorts: &gpu_head_ports - - {containerPort: 6379, name: gcs-server} - - {containerPort: 8265, name: dashboard} - - {containerPort: 10001, name: client} - dshmMounts: &dshm_mounts - - {mountPath: /dev/shm, name: dshm} - dshm16: &dshm_16 - - name: dshm - emptyDir: {medium: Memory, sizeLimit: 16Gi} - dshm64: &dshm_64 - - name: dshm - emptyDir: {medium: Memory, sizeLimit: 64Gi} - -# namespace intentionally omitted — CLI auto-infers `default` from kube context. -image: nvcr.io/nvidian/nemo-rl:nightly -imagePullSecrets: [nvcr-secret] -serviceAccount: nemo-rl-endpoint-registry - -labels: - nrl-k8s/owner: ${user:} - -launch: - mode: attach - attach: - training: ${user:}-raycluster-single-qwen3-4b-gb300 - peerWatcher: false - entrypoint: | - pip install kubernetes -q || true - # Data files are pre-staged on the pods via kubectl cp. - export DISAGG_TRAIN_PATH=/tmp/train.jsonl - export DISAGG_VALID_PATH=/tmp/validation.jsonl - # NOTE: PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True is incompatible - # with colocated vLLM's CuMemAllocator (AssertionError at vLLM init). - export RAY_enable_infeasible_task_early_exit=true - export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM - python -u examples/nemo_gym/run_grpo_nemo_gym.py \ - --config nrl_k8s_run.yaml \ - ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ - ~policy.optimizer.kwargs.foreach \ - ~policy.optimizer.kwargs.fused - env: {} - rayUploadPaths: - - nemo_rl - - examples - - infra/examples - - tests/check_metrics.py - - tests/json_dump_tb_logs.py - - 3rdparty/Gym-workspace/Gym/nemo_gym - - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model - - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent - - 3rdparty/Gym-workspace/Gym/pyproject.toml - - 3rdparty/Gym-workspace/Gym/uv.lock - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/app.py - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/requirements.txt - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/tests - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/validation.jsonl - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example.jsonl - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example_rollouts.jsonl - - 3rdparty/Megatron-LM-workspace/Megatron-LM/megatron - -clusters: - # ---------------- Training (GPU head + worker: generation + gym colocated) ---------------- - training: - name: ${user:}-raycluster-single-qwen3-4b-gb300 - labels: - disagg.nemo-rl/cluster: single-qwen3-4b-gb300 - disagg.nemo-rl/run: qwen3-4b-single-gb300 - spec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - dnsPolicy: ClusterFirst - nodeSelector: *shared_node_selector - containers: - - name: ray-head - env: - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - - name: WANDB_API_KEY - valueFrom: - secretKeyRef: - name: wandb-api-key - key: WANDB_API_KEY - resources: *shared_head_resources - ports: *gpu_head_ports - volumeMounts: *dshm_mounts - volumes: *dshm_16 - workerGroupSpecs: - - groupName: gpu-workers - replicas: 1 - minReplicas: 1 - maxReplicas: 1 - rayStartParams: {num-gpus: "4", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - hostNetwork: true - dnsPolicy: ClusterFirstWithHostNet - nodeSelector: *shared_node_selector - containers: - - name: ray-worker - env: *shared_worker_env - resources: *gpu_worker_resources - volumeMounts: *dshm_mounts - volumes: *dshm_64 diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml deleted file mode 100644 index abb983c2b97..00000000000 --- a/tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml +++ /dev/null @@ -1,181 +0,0 @@ -# Prod-mode infra for qwen3_4b_if_single.yaml on GB300. -# -# Same topology as qwen3_4b_if_single.gb300.infra.yaml (1 node × 4 GPUs on -# p6e-gb300r.36xlarge), but submission is prod-shaped: -# -# submit.submitter portForward — Ray Job SDK via kubectl port-forward. -# launch.runMode batch — CLI returns after Ray accepts the job. -# launch.codeSource image — no working_dir upload; code already -# on the pod's filesystem. -# launch.codePath /opt/nemo-rl — the RL repo lives here inside every -# head + worker pod, served off the -# shared FSx Lustre PVC via subPath. -# -# Because /opt/nemo-rl is the Lustre checkout at ${user:}/nemo-rl, edits on Lustre -# show up in the next submission without rebuilding the image. That's the whole -# point of prod mode: iterate on code without churning the container. -# -# Prereqs (onboarding §3–§5 already applied in `default`): -# - rl-workspace PVC bound (shared FSx Lustre, 2.4 TiB, RWX) -# - /mnt/rl-workspace/${user:}/nemo-rl contains a git checkout of -# https://github.com/NVIDIA-NeMo/RL on branch hemil/k8s-infra-cp -# - nvcr-secret, wandb-api-key, nemo-rl-endpoint-registry all present -# -# Usage: -# # Long-lived cluster — idempotent; reuses the live cluster if it -# # already matches, applies if absent. Submits training per invocation. -# nrl-k8s run tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml \ -# --run-id single-if-$(date +%Y%m%d-%H%M%S) -# -# # One-shot with auto-teardown (ephemeral RayCluster): -# nrl-k8s rayjob tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml - -_shared: - nodeSelector: &shared_node_selector - nvidia.com/gpu.product: NVIDIA-GB300 - headEnv: &shared_head_env - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - workerEnv: &shared_worker_env - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - headResources: &shared_head_resources - limits: {cpu: "8", memory: "32Gi"} - requests: {cpu: "2", memory: "8Gi"} - gpuWorkerResources: &gpu_worker_resources - # p6e-gb300r.36xlarge allocatable: ~139.6 CPU / ~924 GiB / 4 GPUs. - limits: - cpu: "128" - memory: "880Gi" - nvidia.com/gpu: "4" - requests: - cpu: "128" - memory: "880Gi" - nvidia.com/gpu: "4" - gpuHeadPorts: &gpu_head_ports - - {containerPort: 6379, name: gcs-server} - - {containerPort: 8265, name: dashboard} - - {containerPort: 10001, name: client} - # Volumes. The Lustre PVC is mounted at /opt/nemo-rl (subPath ${user:}/nemo-rl) - # for code, and at /mnt/rl-workspace (no subPath) for run outputs, caches, - # and checkpoints. dshm stays as an emptyDir for Ray's object store. - codeMounts: &code_mounts - - {mountPath: /dev/shm, name: dshm} - - {mountPath: /opt/nemo-rl, name: rl-workspace, subPath: "${user:}/nemo-rl"} - - {mountPath: /mnt/rl-workspace, name: rl-workspace} - headVolumes: &head_volumes - - name: dshm - emptyDir: {medium: Memory, sizeLimit: 16Gi} - - name: rl-workspace - persistentVolumeClaim: {claimName: rl-workspace} - workerVolumes: &worker_volumes - - name: dshm - emptyDir: {medium: Memory, sizeLimit: 64Gi} - - name: rl-workspace - persistentVolumeClaim: {claimName: rl-workspace} - -# namespace auto-inferred from kube context (default). -image: nvcr.io/nvidian/nemo-rl:nightly -imagePullSecrets: [nvcr-secret] -serviceAccount: nemo-rl-endpoint-registry - -labels: - nrl-k8s/owner: ${user:} - -submit: - submitter: portForward - -launch: - mode: attach - runMode: batch - codeSource: image - codePath: /opt/nemo-rl - attach: - training: ${user:}-raycluster-single-qwen3-4b-gb300-prod - peerWatcher: false - env: {} - # Runs inside the training head pod under Ray's Job SDK. cwd inherits - # from the pod's default, so `cd /opt/nemo-rl` is explicit. The config - # path is repo-relative — load_config follows the recipe's `defaults:` - # up to grpo_qwen3_4b_instruct_k8s_base.yaml automatically. - entrypoint: | - set -eu - cd /opt/nemo-rl - if [ ! -d /mnt/rl-workspace ]; then - echo "ERROR: /mnt/rl-workspace not mounted — is the rl-workspace PVC bound?" >&2 - exit 1 - fi - # Full instruction-following dataset (~20K prompts) snapshotted from - # huggingface.co/datasets/nvidia/Nemotron-RL-instruction_following onto the - # shared Lustre workspace. - IF_DATA=/mnt/rl-workspace/${user:}/datasets/instruction_following/instruction_following.jsonl - export DISAGG_TRAIN_PATH=$IF_DATA - export DISAGG_VALID_PATH=$IF_DATA - export RAY_enable_infeasible_task_early_exit=true - export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM - LOG_DIR=/mnt/rl-workspace/${user:}/driver_logs - LOG=\${LOG_DIR}/${user:}-raycluster-single-qwen3-4b-gb300-prod-training-$(date -u +%Y%m%d-%H%M%S-%N).log - mkdir -p "\${LOG_DIR}" - - python -u examples/nemo_gym/run_grpo_nemo_gym.py \ - --config tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ - ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ - ~policy.optimizer.kwargs.foreach \ - ~policy.optimizer.kwargs.fused \ - 2>&1 | tee "\${LOG}" - -clusters: - training: - name: ${user:}-raycluster-single-qwen3-4b-gb300-prod - labels: - disagg.nemo-rl/cluster: single-qwen3-4b-gb300-prod - disagg.nemo-rl/run: qwen3-4b-single-gb300-prod - spec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - dnsPolicy: ClusterFirst - nodeSelector: *shared_node_selector - containers: - - name: ray-head - env: - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - - name: WANDB_API_KEY - valueFrom: - secretKeyRef: - name: wandb-api-key - key: WANDB_API_KEY - resources: *shared_head_resources - ports: *gpu_head_ports - volumeMounts: *code_mounts - volumes: *head_volumes - workerGroupSpecs: - - groupName: gpu-workers - replicas: 1 - minReplicas: 1 - maxReplicas: 1 - rayStartParams: {num-gpus: "4", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - hostNetwork: true - dnsPolicy: ClusterFirstWithHostNet - nodeSelector: *shared_node_selector - containers: - - name: ray-worker - env: *shared_worker_env - resources: *gpu_worker_resources - volumeMounts: *code_mounts - volumes: *worker_volumes diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml deleted file mode 100644 index de51edf3722..00000000000 --- a/tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml +++ /dev/null @@ -1,169 +0,0 @@ -# Infra for single-cluster Qwen3-4B colocated GRPO run. -# -# A single RayCluster hosting training + colocated vLLM generation + a local -# Gym Ray actor. No separate gym or generation cluster — that's the whole -# point of single-cluster: the training entrypoint calls -# create_env(env_name="nemo_gym", ...) -# which pins the Gym actor to a non-head worker via NodeAffinityScheduling, -# so we must size the GPU worker big enough to host *both* the vLLM engines -# (8 GPU) and the Gym actor (~4 CPU). Namespace is auto-inferred from the -# kube context (nemo-rl-testing). - -_shared: - nodeSelector: &shared_node_selector - gpu-wrangler.nvidia.com/lease: nemo-rl-testing - tolerations: &shared_tolerations - - {key: gpu-wrangler.nvidia.com/lease, operator: Equal, value: nemo-rl-testing, effect: NoSchedule} - - {key: platform.nvidia.com/gpu, operator: Equal, value: "true", effect: NoSchedule} - # Image ships aws-ofi + enp71s0 defaults; keep pod env minimal. - headEnv: &shared_head_env - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - workerEnv: &shared_worker_env - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: GLOO_SOCKET_IFNAME, value: enp71s0} - - {name: NCCL_SOCKET_IFNAME, value: enp71s0} - - {name: FI_PROVIDER, value: efa} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - headResources: &shared_head_resources - limits: {cpu: "8", memory: "32Gi"} - requests: {cpu: "2", memory: "8Gi"} - gpuWorkerResources: &gpu_worker_resources - # Sized for the full p5.48xlarge (192 CPU / ~1957 GiB allocatable). - # Single-cluster needs the worker to host training + colocated vLLM + - # the Gym actor (~4 CPU). We claim 184 CPU / 1850 GiB (leaving ~8 CPU + - # ~100 GiB for the Ray head pod + k8s daemonsets: kube-proxy, aws-node, - # nvidia-device-plugin, node-exporter). 32 EFA devices = full NIC set. - limits: - cpu: "184" - memory: "1850Gi" - nvidia.com/gpu: "8" - vpc.amazonaws.com/efa: "32" - requests: - cpu: "184" - memory: "1850Gi" - nvidia.com/gpu: "8" - vpc.amazonaws.com/efa: "32" - gpuHeadPorts: &gpu_head_ports - - {containerPort: 6379, name: gcs-server} - - {containerPort: 8265, name: dashboard} - - {containerPort: 10001, name: client} - dshmMounts: &dshm_mounts - - {mountPath: /dev/shm, name: dshm} - dshm16: &dshm_16 - - name: dshm - emptyDir: {medium: Memory, sizeLimit: 16Gi} - dshm64: &dshm_64 - - name: dshm - emptyDir: {medium: Memory, sizeLimit: 64Gi} - -# namespace intentionally omitted — CLI auto-infers nemo-rl-testing from -# the current kube context. -image: nvcr.io/nvidian/nemo-rl:nightly -imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] -serviceAccount: nemo-rl-endpoint-registry - -labels: - nrl-k8s/owner: ${user:} - -launch: - mode: attach - attach: - training: ${user:}-raycluster-single-qwen3-4b - peerWatcher: false - # Single-cluster: no remote_generation_url (colocated), no disagg_job_id - # (no endpoint registry — gym is a local Ray actor). - entrypoint: | - pip install kubernetes -q || true - export GLOO_SOCKET_IFNAME=enp71s0 - export NCCL_SOCKET_IFNAME=enp71s0 - export FI_PROVIDER=efa - # Data files are pre-staged on the pods via kubectl cp. - export DISAGG_TRAIN_PATH=/tmp/train.jsonl - export DISAGG_VALID_PATH=/tmp/validation.jsonl - # NOTE: PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True is incompatible - # with colocated vLLM's CuMemAllocator (AssertionError at vLLM init). - export RAY_enable_infeasible_task_early_exit=true - export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM - python -u examples/nemo_gym/run_grpo_nemo_gym.py \ - --config nrl_k8s_run.yaml \ - ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ - ~policy.optimizer.kwargs.foreach \ - ~policy.optimizer.kwargs.fused - env: {} - rayUploadPaths: - - nemo_rl - - examples - - infra/examples - - tests/check_metrics.py - - tests/json_dump_tb_logs.py - - 3rdparty/Gym-workspace/Gym/nemo_gym - - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model - - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent - - 3rdparty/Gym-workspace/Gym/pyproject.toml - - 3rdparty/Gym-workspace/Gym/uv.lock - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/app.py - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/requirements.txt - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/tests - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/validation.jsonl - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example.jsonl - - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example_rollouts.jsonl - - 3rdparty/Megatron-LM-workspace/Megatron-LM/megatron - -clusters: - # ---------------- Training (GPU head + worker: generation + gym colocated) ---------------- - training: - name: ${user:}-raycluster-single-qwen3-4b - labels: - disagg.nemo-rl/cluster: single-qwen3-4b - disagg.nemo-rl/run: qwen3-4b-single - spec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - dnsPolicy: ClusterFirst - nodeSelector: *shared_node_selector - tolerations: *shared_tolerations - containers: - - name: ray-head - env: - - {name: HF_HOME, value: /tmp/huggingface} - - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} - - {name: NCCL_DEBUG, value: INFO} - - {name: RAY_memory_monitor_refresh_ms, value: "0"} - - name: WANDB_API_KEY - valueFrom: - secretKeyRef: - name: wandb-api-key - key: WANDB_API_KEY - resources: *shared_head_resources - ports: *gpu_head_ports - volumeMounts: *dshm_mounts - volumes: *dshm_16 - workerGroupSpecs: - - groupName: gpu-workers - replicas: 1 - minReplicas: 1 - maxReplicas: 1 - rayStartParams: {num-gpus: "8", object-store-memory: "200000000"} - template: - spec: - schedulerName: default-scheduler - hostNetwork: true - dnsPolicy: ClusterFirstWithHostNet - nodeSelector: *shared_node_selector - tolerations: *shared_tolerations - containers: - - name: ray-worker - env: *shared_worker_env - resources: *gpu_worker_resources - volumeMounts: *dshm_mounts - volumes: *dshm_64 diff --git a/tools/nrl_k8s/examples/qwen3_4b_if_single.yaml b/tools/nrl_k8s/examples/qwen3_4b_if_single.yaml deleted file mode 100644 index 9ec4dd473af..00000000000 --- a/tools/nrl_k8s/examples/qwen3_4b_if_single.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Qwen3-4B GRPO on instruction_following gym — **single RayCluster variant**. -# -# Training, colocated vLLM generation, AND gym all run inside a single -# RayCluster. Gym is spawned as a local Ray actor by -# examples/nemo_gym/run_grpo_nemo_gym.py (create_env(env_name="nemo_gym", ...)) -# because neither env.disagg_job_id nor env.remote_gym_url is set. -# -# Pair with qwen3_4b_if_single.infra.yaml (declares only the training cluster; -# no separate gym or generation cluster). -defaults: ../../../examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml - -cluster: - gpus_per_node: 4 - num_nodes: 1 -grpo: - num_prompts_per_step: 32 - num_generations_per_prompt: 16 - max_num_steps: 500 - val_period: 50 - max_rollout_turns: 1 - async_grpo: - # NeMo-RL asserts non-colocated when async_grpo is enabled. Single-cluster runs - # colocated generation, so GRPO must remain synchronous. - enabled: false -policy: - train_global_batch_size: 256 - train_micro_batch_size: 1 - logprob_batch_size: 1 - # Colocated vLLM + backward on 4B takes too much GPU at 16K context; - # halving leaves headroom for vLLM's KV cache + activations + optimizer. - max_total_sequence_length: 8192 - dtensor_cfg: - activation_checkpointing: true - generation: - colocated: - enabled: true - resources: - gpus_per_node: 4 - num_nodes: 1 - vllm_cfg: - gpu_memory_utilization: 0.45 # leave 55% for training state -checkpointing: - save_period: 50 -logger: - wandb_enabled: true - wandb: - project: nemorl-single-k8s - name: single-if-qwen3-4b - tensorboard_enabled: true - monitor_gpus: true From 3f91cd1eb9cda6a753d5289f8037ba7dcc46e913 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 22:30:21 -0700 Subject: [PATCH 51/84] chore: remove non-upstream docs and redundant examples Signed-off-by: Terry Kong --- infra/examples/monolithic-jobset-no-kai.yaml | 276 ----------- kubernetes-onboarding.md | 489 ------------------- tools/nrl_k8s/docs/onboarding.md | 308 ------------ tools/nrl_k8s/docs/production-readiness.md | 134 ----- tools/nrl_k8s/docs/recipes.md | 419 ---------------- tools/nrl_k8s/docs/roadmap.md | 111 ----- 6 files changed, 1737 deletions(-) delete mode 100644 infra/examples/monolithic-jobset-no-kai.yaml delete mode 100644 kubernetes-onboarding.md delete mode 100644 tools/nrl_k8s/docs/onboarding.md delete mode 100644 tools/nrl_k8s/docs/production-readiness.md delete mode 100644 tools/nrl_k8s/docs/recipes.md delete mode 100644 tools/nrl_k8s/docs/roadmap.md diff --git a/infra/examples/monolithic-jobset-no-kai.yaml b/infra/examples/monolithic-jobset-no-kai.yaml deleted file mode 100644 index ee1ef138c1a..00000000000 --- a/infra/examples/monolithic-jobset-no-kai.yaml +++ /dev/null @@ -1,276 +0,0 @@ -# Same as monolithic-jobset.yaml but WITHOUT KAI scheduler. -# Uses default-scheduler so your colleague can diff the two and compare -# pod specs, resource claim allocation, and CDI device injection. -# -# The only differences from monolithic-jobset.yaml: -# - No kai.scheduler/queue label -# - No schedulerName: kai-scheduler on pod specs -# - Resource names suffixed with "-no-kai" to avoid conflicts -# -# Usage: -# kubectl apply -f monolithic-jobset-no-kai.yaml -# kubectl get jobset monolithic-job-no-kai -w - -# --- 1. ComputeDomain (controller auto-creates the ResourceClaimTemplate) --- -apiVersion: resource.nvidia.com/v1beta1 -kind: ComputeDomain -metadata: - name: compute-domain-monolithic-no-kai -spec: - channel: - resourceClaimTemplate: - name: compute-domain-monolithic-no-kai - numNodes: 0 ---- -# --- 2. RoCE ResourceClaimTemplate (manually created) --- -apiVersion: resource.k8s.io/v1 -kind: ResourceClaimTemplate -metadata: - name: roce-monolithic-no-kai -spec: - spec: - devices: - requests: - - exactly: - count: 8 - deviceClassName: roce.networking.k8s.aws - name: roce ---- -# --- JobSet --- -apiVersion: jobset.x-k8s.io/v1alpha2 -kind: JobSet -metadata: - name: monolithic-job-no-kai -spec: - network: - enableDNSHostnames: true - publishNotReadyAddresses: true - - coordinator: - replicatedJob: head - jobIndex: 0 - podIndex: 0 - - successPolicy: - operator: All - targetReplicatedJobs: [driver] - - failurePolicy: - maxRestarts: 0 - rules: - - name: head_crash - action: FailJobSet - targetReplicatedJobs: [head] - - name: driver_crash - action: FailJobSet - targetReplicatedJobs: [driver] - - name: worker_crash - action: FailJobSet - targetReplicatedJobs: [workers] - - replicatedJobs: - # ======================== - # Ray head (CPU-only) - # ======================== - - name: head - replicas: 1 - template: - spec: - backoffLimit: 0 - completions: 1 - parallelism: 1 - template: - spec: - imagePullSecrets: - - name: nvcr-secret - tolerations: - - operator: Exists - containers: - - name: ray-head - image: nvcr.io/nvidian/nemo-rl:nightly - command: ["/bin/bash", "-c"] - args: - - | - ulimit -n 65536 - ray start --head --port=6379 \ - --dashboard-host=0.0.0.0 \ - --num-gpus=0 \ - --object-store-memory=200000000 \ - --block - ports: - - { containerPort: 6379, name: gcs-server } - - { containerPort: 8265, name: dashboard } - - { containerPort: 10001, name: client } - readinessProbe: - exec: - command: ["ray", "health-check"] - initialDelaySeconds: 10 - periodSeconds: 5 - timeoutSeconds: 5 - resources: - requests: { cpu: "2", memory: "8Gi" } - limits: { cpu: "8", memory: "32Gi" } - env: - - { name: RAY_ADDRESS, value: "127.0.0.1:6379" } - - { name: RAY_memory_monitor_refresh_ms, value: "0" } - volumeMounts: - - { name: dshm, mountPath: /dev/shm } - - { name: rl-workspace, mountPath: /mnt/rl-workspace } - volumes: - - name: dshm - emptyDir: { medium: Memory, sizeLimit: 16Gi } - - name: rl-workspace - persistentVolumeClaim: { claimName: rl-workspace } - - # ======================== - # GPU workers (4 pods × 4 GPUs = 16 GPUs) - # ======================== - - name: workers - replicas: 1 - template: - spec: - backoffLimit: 0 - completions: 4 - parallelism: 4 - template: - spec: - imagePullSecrets: - - name: nvcr-secret - tolerations: - - operator: Exists - initContainers: - - name: wait-for-head - image: nvcr.io/nvidian/nemo-rl:nightly - command: ["/bin/bash", "-c"] - args: - - | - HEAD=monolithic-job-no-kai-head-0-0.monolithic-job-no-kai - echo "Waiting for head at ${HEAD}:6379..." - until ray health-check --address ${HEAD}:6379 >/dev/null 2>&1; do - sleep 5 - done - echo "Head is ready." - resources: - requests: { cpu: "200m", memory: "256Mi" } - containers: - - name: ray-worker - image: nvcr.io/nvidian/nemo-rl:nightly - command: ["/bin/bash", "-c"] - args: - - | - ulimit -n 65536 - HEAD=monolithic-job-no-kai-head-0-0.monolithic-job-no-kai - ray start --address=${HEAD}:6379 \ - --num-gpus=4 \ - --object-store-memory=200000000 \ - --block - resources: - claims: - - name: compute-domain-channel - - name: roce-channel - requests: - cpu: "32" - memory: "200Gi" - nvidia.com/gpu: "4" - limits: - cpu: "128" - memory: "800Gi" - nvidia.com/gpu: "4" - env: - - { name: RAY_ADDRESS, value: "monolithic-job-no-kai-head-0-0.monolithic-job-no-kai:6379" } - - { name: RAY_memory_monitor_refresh_ms, value: "0" } - - { name: HF_HOME, value: /mnt/rl-workspace/shared/hf-cache } - - { name: HF_DATASETS_CACHE, value: /mnt/rl-workspace/shared/hf-cache/datasets } - - { name: TRANSFORMERS_CACHE, value: /mnt/rl-workspace/shared/hf-cache } - - { name: NCCL_DEBUG, value: INFO } - - { name: NCCL_MNNVL_ENABLE, value: "1" } - volumeMounts: - - { name: dshm, mountPath: /dev/shm } - - { name: rl-workspace, mountPath: /mnt/rl-workspace } - securityContext: - capabilities: - add: [IPC_LOCK] - resourceClaims: - - name: compute-domain-channel - resourceClaimTemplateName: compute-domain-monolithic-no-kai - - name: roce-channel - resourceClaimTemplateName: roce-monolithic-no-kai - volumes: - - name: dshm - emptyDir: { medium: Memory, sizeLimit: 64Gi } - - name: rl-workspace - persistentVolumeClaim: { claimName: rl-workspace } - - # ======================== - # Driver (submits SFT training, gates success) - # ======================== - - name: driver - replicas: 1 - template: - spec: - backoffLimit: 0 - completions: 1 - parallelism: 1 - template: - spec: - imagePullSecrets: - - name: nvcr-secret - tolerations: - - operator: Exists - initContainers: - - name: wait-for-head - image: nvcr.io/nvidian/nemo-rl:nightly - command: ["/bin/bash", "-c"] - args: - - | - HEAD=monolithic-job-no-kai-head-0-0.monolithic-job-no-kai - echo "Waiting for head at ${HEAD}:6379..." - until ray health-check --address ${HEAD}:6379 >/dev/null 2>&1; do - sleep 5 - done - echo "Head is ready." - resources: - requests: { cpu: "200m", memory: "256Mi" } - containers: - - name: driver - image: nvcr.io/nvidian/nemo-rl:nightly - command: ["/bin/bash", "-c"] - args: - - | - set -eo pipefail - HEAD=monolithic-job-no-kai-head-0-0.monolithic-job-no-kai - SUBMISSION_ID="sft-$(date +%s)" - - ray job submit --address http://${HEAD}:8265 \ - --submission-id ${SUBMISSION_ID} \ - -- bash -c " - cd /opt/nemo-rl - export HF_HOME=/mnt/rl-workspace/shared/hf-cache - export HF_DATASETS_CACHE=/mnt/rl-workspace/shared/hf-cache/datasets - export TRANSFORMERS_CACHE=/mnt/rl-workspace/shared/hf-cache - - python -u examples/run_sft.py \ - --config examples/configs/sft.yaml \ - policy.model_name=Qwen/Qwen3-0.6B \ - cluster.gpus_per_node=4 \ - cluster.num_nodes=4 \ - policy.train_global_batch_size=32 \ - policy.train_micro_batch_size=1 \ - policy.max_total_sequence_length=1024 \ - policy.dtensor_cfg.tensor_parallel_size=1 \ - sft.max_num_steps=10 \ - sft.val_period=5 \ - checkpointing.enabled=false \ - logger.wandb_enabled=false \ - logger.tensorboard_enabled=false - " 2>&1 - resources: - requests: { cpu: "500m", memory: "2Gi" } - limits: { cpu: "2", memory: "8Gi" } - env: - - { name: RAY_ADDRESS, value: "monolithic-job-no-kai-head-0-0.monolithic-job-no-kai:6379" } - volumeMounts: - - { name: rl-workspace, mountPath: /mnt/rl-workspace } - volumes: - - name: rl-workspace - persistentVolumeClaim: { claimName: rl-workspace } diff --git a/kubernetes-onboarding.md b/kubernetes-onboarding.md deleted file mode 100644 index 2ba46358c57..00000000000 --- a/kubernetes-onboarding.md +++ /dev/null @@ -1,489 +0,0 @@ -# Kubernetes Onboarding - -## Prerequisites - -### Install kubectl - -```bash -# macOS -brew install kubectl - -# Linux (amd64) -curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" -chmod +x kubectl && sudo mv kubectl /usr/local/bin/ -``` - -### Install kubectx and kubens - -`kubectx` switches between clusters; `kubens` switches the default namespace -for the current context. - -```bash -# macOS -brew install kubectx - -# Linux -sudo apt install kubectx -# or from source: -git clone https://github.com/ahmetb/kubectx ~/.kubectx -ln -s ~/.kubectx/kubectx ~/.local/bin/kubectx -ln -s ~/.kubectx/kubens ~/.local/bin/kubens -``` - -### Install nrl-k8s - -The `nrl-k8s` CLI launches NeMo-RL training jobs on Kubernetes. It lives -under `tools/nrl_k8s/` and will be available on `main` soon. Until then, -install from the `hemil/k8s-infra-cp` branch: - -```bash -uv tool install "nrl-k8s @ git+https://github.com/NVIDIA-NeMo/RL.git@hemil/k8s-infra-cp#subdirectory=tools/nrl_k8s" -nrl-k8s --version -``` - -If you have the repo checked out locally, you can install from whatever -branch you're on: - -```bash -uv tool install ./tools/nrl_k8s -``` - -In both cases, reinstall to pick up changes: - -```bash -uv tool install --reinstall ./tools/nrl_k8s -``` - -### Install AWS CLI v2 - -Required for EKS clusters (nemo-ci-h100, aws-cmh). - -```bash -# macOS -brew install awscli - -# Linux -curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" -unzip awscliv2.zip && sudo ./aws/install -``` - ---- - -## 1. aws-cmh (GB300) - -### 1.1 Requesting access - -Request access to the AWS account via one of the following DLs: - -- **Admins:** [access-aws-nemo-rl-dev-admin](https://dlrequest/GroupID/Groups/Properties?identity=MzI2NjViN2ViMjdkNGQ0ZGEwMWYxYjhiMmMzN2E0NGJ8Z3JvdXA=) -- **Users:** [access-aws-nemo-rl-dev-engineer](https://dlrequest/GroupID/Groups/Properties?identity=MDVmMzI0OTc5ZDhmNDk5ZWI3MjlkY2E1ZjAxOWVkMDZ8Z3JvdXA=) - -> **Note:** After your DL request is approved, permissions sync on the hour, -> so you may need to wait up to an hour before you can log in. - -### 1.2 Logging in - -Authenticate with AWS using `nvsec`: - -```bash -nvsec aws auth -``` - -If you're on a remote machine (e.g. SSH'd into a devbox), use the -no-browser flag: - -```bash -nvsec aws auth --no-browser -``` - -This will print a URL like: - -``` -https://awscloud.nvidia.com/cli-login?redirect_uri=http://localhost:53682/callback&state=...&client=nvsec -``` - -Note the port number in the URL (e.g. `53682`). Before opening the URL, -set up a port forward from your local machine so the callback can reach -the remote host: - -```bash -ssh -L 53682:localhost:53682 -``` - -Then open the URL in your local browser. The auth should complete in your -terminal. - -### 1.3 Selecting the AWS profile - -List available accounts: - -```bash -nvsec aws list -``` - -You should see `nemo-rl-dev` listed: - -``` -Available roles (3): - - nemo-rl-dev - Account: 942195279341 | MPA: DGX_CLOUD_MPA - 0) CS-Admin - 1) CS-Engineer-942195279341 - - NeMo_Megatron - Account: 766267172432 | MPA: DGX_CLOUD_MPA - 2) CS-Admin -``` - -Configure credentials for your access level. When prompted for a profile -name, press Enter to accept the default: - -```bash -nvsec aws configure 0 # CS-Admin -nvsec aws configure 1 # CS-Engineer -``` - -### 1.4 Setting up your kubeconfig - -Add the EKS cluster to your kubeconfig: - -```bash -aws eks update-kubeconfig \ - --name ltqlfcnzyr-dgxc-k8s-aws-use2-prod \ - --region us-east-2 -``` - -The command creates a context with a long ARN name. Create a friendly alias: - -```bash -kubectx aws-cmh=arn:aws:eks:us-east-2:942195279341:cluster/ltqlfcnzyr-dgxc-k8s-aws-use2-prod -``` - -Switch to the context: - -```bash -kubectx aws-cmh -``` - -Verify access: - -```bash -kubectl get nodes # should list a bunch of nodes -kubectl auth can-i create rayclusters # should print "yes" -``` - -### 1.5 Shared workspace (PVC) - -The `default` namespace has a shared FSx Lustre PVC called `rl-workspace` -(1.2 TiB, ReadWriteMany). All pods across all nodes can read and write to -it simultaneously. - -The PVC already exists — you don't need to create it. If it ever needs to -be recreated: - -```bash -kubectl apply -f - <<'EOF' -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: rl-workspace - namespace: default -spec: - accessModes: - - ReadWriteMany - storageClassName: dgxc-enterprise-file - resources: - requests: - storage: 1200Gi -EOF -``` - -> **Note:** FSx Lustre has a minimum size of 1.2 TiB. Provisioning takes -> 5–15 minutes while AWS creates the Lustre filesystem. Watch progress with: -> -> ```bash -> kubectl get pvc rl-workspace -w -> ``` -> -> It will show `Pending` until the filesystem is ready, then flip to `Bound`. -> If you see `ProvisioningFailed` / `DeadlineExceeded` events, that's normal -> — the CSI driver retries automatically. - -Verify: - -```bash -kubectl get pvc rl-workspace # should show Bound, 1200Gi, RWX, dgxc-enterprise-file -``` - -To resize (only works after the PVC is `Bound` — if you try while -`Pending`, you'll get): - -``` -The PersistentVolumeClaim "rl-workspace" is invalid: spec: Forbidden: spec is immutable after creation except resources.requests and volumeAttributesClassName for bound claims -``` - -Once bound, resize with: - -```bash -kubectl patch pvc rl-workspace -p '{"spec":{"resources":{"requests":{"storage":"2400Gi"}}}}' -``` - -FSx Lustre grows in increments of 2.4 TiB. - -This PVC is shared across the team. To avoid collisions, organize by -username: - -``` -/mnt/rl-workspace/ -├── terryk/ -│ ├── data/ -│ ├── checkpoints/ -│ └── hf-cache/ -├── hemild/ -│ └── ... -└── shared/ - └── models/ -``` - -You can create additional PVCs if needed, but data cannot be shared across -different PVCs. To deduplicate things like HuggingFace model downloads, it's -easier to start with this shared PVC and only create a separate one if you -need isolation. - -The PVC uses `reclaimPolicy: Retain`, so data persists even if pods are -deleted. Do not delete the PVC itself — it's shared across the team. - -## 2. nemo-ci-h100 - -### 2.1 Requesting access - -TODO - -### 2.2 Logging in - -Authenticate with AWS SSO using the `megatron` profile: - -```bash -aws sso login --profile megatron -``` - -This will attempt to open your browser. If you're on a remote machine (e.g. -SSH'd into a devbox), the browser won't open and you'll see something like: - -``` -$ aws sso login --profile megatron -Attempting to open your default browser. If the browser does not open, open the following URL. -If you are unable to open the URL on this device, run this command again with the '--use-device-code' option. - -https://oidc.us-east-2.amazonaws.com/authorize?response_type=code&client_id=...&redirect_uri=http%3A%2F%2F127.0.0.1%3A33977%2Foauth%2Fcallback&state=... -``` - -Open that URL in your local browser. It will redirect to a `127.0.0.1` callback -URL, which will fail because the redirect targets the remote machine. Note the -port number in the redirect URL (e.g. `33977`) and set up a port forward from -your local machine: - -```bash -ssh -L 33977:localhost:33977 -``` - -Then refresh the page in your browser. You should see an AWS page that says: - -> Your credentials have been shared successfully and can be used until your -> session expires. You can now close this tab. - -Back in your terminal, the login will complete: - -``` -Successfully logged into Start URL: https://nv-h100.awsapps.com/start -``` - -Verify the session is active: - -```bash -aws sts get-caller-identity --profile megatron -``` - -You should see something like: - -```json -{ - "UserId": "AROA3E2IVEZIKPBBBR34J:terryk@nvidia.com", - "Account": "766267172432", - "Arn": "arn:aws:sts::766267172432:assumed-role/AWSReservedSSO_CS-Admin_a7cbef6db22f1b0b/terryk@nvidia.com" -} -``` - -You'll need to re-run `aws sso login` whenever your SSO token expires -(typically every 8–12 hours). - -### 2.3 Setting up your kubeconfig - -Add the EKS cluster to your kubeconfig and create a friendly context name: - -```bash -aws eks update-kubeconfig \ - --region us-east-1 \ - --name geyydnzzhv-dgxc-k8s-aws-use1-prod \ - --profile megatron \ - --alias nemo-ci-h100 -``` - -Switch to the context and set the default namespace: - -```bash -kubectx nemo-ci-h100 -kubens nemo-rl-testing -``` - -Verify access: - -```bash -kubectl get nodes # should list a bunch of nodes -kubectl auth can-i create rayclusters # should print "yes" -``` - -### 2.4 Shared workspace (PVC) - -The `nemo-rl-testing` namespace has a shared EFS PVC called `rl-workspace` -(100 GiB, ReadWriteMany). It's backed by AWS EFS so all pods across all -nodes can read and write to it simultaneously. - -The PVC already exists — you don't need to create it. If it ever needs to -be recreated: - -```bash -kubectl apply -f - <<'EOF' -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: rl-workspace - namespace: nemo-rl-testing -spec: - accessModes: - - ReadWriteMany - storageClassName: efs-rwx - resources: - requests: - storage: 100Gi -EOF -``` - -Verify: - -```bash -kubectl get pvc rl-workspace # should show Bound, 100Gi, RWX, efs-rwx -``` - -Use this PVC for training data, model caches, checkpoints, and code. To -avoid collisions between users, organize by username: - -``` -/mnt/rl-workspace/ -├── terryk/ -│ ├── data/ # training datasets -│ ├── checkpoints/ # training checkpoints -│ └── hf-cache/ # HuggingFace model cache -├── hemild/ -│ └── ... -└── shared/ - └── models/ # models everyone uses -``` - -To get a shell on the head node and set up your data: - -```bash -kubectl exec -it -- bash - -# Inside the pod: -mkdir -p /mnt/rl-workspace/$(whoami)/{data,checkpoints,hf-cache} -``` - -The PVC uses `reclaimPolicy: Retain`, so data persists even if pods are -deleted. Do not delete the PVC itself — it's shared across the team. - -### 2.5 Running a monolithic RayCluster - -This deploys a single RayCluster with training, colocated vLLM generation, -and gym all on one node. - -Validate the config: - -```bash -nrl-k8s check \ - tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml -``` - -Bring up the cluster: - -```bash -nrl-k8s cluster up \ - tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ - --role training -``` - -Submit training and tail logs. This uploads your local code (the paths -listed in `rayUploadPaths` in the infra YAML) to the Ray cluster as a -working directory, then runs the entrypoint on the cluster: - -```bash -nrl-k8s launch \ - tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ - --follow -``` - -Or do everything in one step (cluster up + job submit): - -```bash -nrl-k8s run \ - tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ - --follow -``` - -Check status: - -```bash -nrl-k8s status \ - tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml -``` - -List and inspect jobs: - -```bash -nrl-k8s job list \ - tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ - --role training - -nrl-k8s job logs \ - tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ - --role training -``` - -Tear down when done: - -```bash -nrl-k8s cluster down \ - tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ - --role training -``` - -## 3. kind - -TODO - -## 4. Cursor setup - -TODO - -## 5. Claude Code setup - -TODO diff --git a/tools/nrl_k8s/docs/onboarding.md b/tools/nrl_k8s/docs/onboarding.md deleted file mode 100644 index 5a8bb5d30ba..00000000000 --- a/tools/nrl_k8s/docs/onboarding.md +++ /dev/null @@ -1,308 +0,0 @@ -# Onboarding a new Kubernetes cluster - -Checklist for going from `kubectl config use-context ` to -`nrl-k8s launch` landing training. Walk through in order; each step -ends with a verify-it-worked probe. - ---- - -## 1. Baseline cluster capabilities - -Required — refuses to install onto clusters that lack these. - -```bash -# KubeRay operator -kubectl get crd rayclusters.ray.io -o jsonpath='{.metadata.name}' && echo " ✓" -kubectl -n kuberay-system get deploy kuberay-operator \ - -o jsonpath='{.status.conditions[?(@.type=="Available")].status}' -# → True - -# GPU device plugin (at least one node reports nvidia.com/gpu in allocatable) -kubectl get nodes \ - -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.allocatable.nvidia\.com/gpu}{"\n"}{end}' -# → one or more rows with non-empty GPU counts -``` - -If KubeRay isn't present, install it. The operator helm chart we use -is tracked in this repo under `infra/helm/`; on a fresh cluster: - -```bash -helm repo add kuberay https://ray-project.github.io/kuberay-helm/ -helm upgrade --install kuberay-operator kuberay/kuberay-operator \ - --namespace kuberay-system --create-namespace \ - --version 1.5.1 -``` - -If your GPU nodes don't yet advertise `nvidia.com/gpu`, install the -NVIDIA device plugin (`kubectl apply -f -https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.15.0/nvidia-device-plugin.yml`) -and re-run the `kubectl get nodes` check. - ---- - -## 2. Create the working namespace - -```bash -NS=nemo-rl-testing # pick whatever fits your org -kubectl create namespace $NS -kubectl config set-context --current --namespace=$NS -``` - -`nrl-k8s` reads the active namespace from your kube context by -default, so setting it on the context saves you from `-n $NS` on -every call. - ---- - -## 3. RBAC — the endpoint-registry ServiceAccount - -Disaggregated runs rendezvous on a ConfigMap called -`nemo-rl-endpoints-`, written and read by the training + -generation + gym pods via the Kubernetes Python client. Each pod -needs `get / list / watch / create / update / patch` on ConfigMaps -in its namespace, and (if the peer-watcher sidecar is enabled) -`get / delete` on `rayclusters.ray.io` too. - -Apply the bundled RBAC manifest: - -```bash -kubectl apply -n $NS -f - <<'EOF' -apiVersion: v1 -kind: ServiceAccount -metadata: - name: nemo-rl-endpoint-registry ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: nemo-rl-endpoint-registry -rules: - - apiGroups: [""] - resources: [configmaps] - verbs: [get, list, watch, create, update, patch, delete] - - apiGroups: [ray.io] - resources: [rayclusters] - verbs: [get, list, watch, delete] -EOF -kubectl create rolebinding -n $NS nemo-rl-endpoint-registry \ - --role=nemo-rl-endpoint-registry \ - --serviceaccount=$NS:nemo-rl-endpoint-registry -``` - -Verify: - -```bash -kubectl auth can-i create configmaps --as=system:serviceaccount:$NS:nemo-rl-endpoint-registry -kubectl auth can-i delete rayclusters.ray.io --as=system:serviceaccount:$NS:nemo-rl-endpoint-registry -# → yes, yes -``` - ---- - -## 4. Image pull secrets (private registries) - -If your image lives in a private registry (e.g. `nvcr.io/nvidian/...`), -create a Docker-config secret: - -```bash -kubectl create secret docker-registry ngc-registry-secret \ - --docker-server=nvcr.io \ - --docker-username='$oauthtoken' \ - --docker-password="$NGC_API_KEY" \ - -n $NS -``` - -The recipe's `infra.imagePullSecrets` field references these by name: - -```yaml -imagePullSecrets: [ngc-registry-secret] -``` - ---- - -## 5. W&B secret (optional) - -If your recipe logs to Weights & Biases, pods need `WANDB_API_KEY`. -We consume it via a `secretKeyRef` rather than embedding the key in -YAML — so rotate/replace the secret without touching any recipe: - -```bash -kubectl create secret generic wandb-api-key \ - --from-literal=WANDB_API_KEY="$WANDB_API_KEY" \ - -n $NS -``` - -The existing example infra files reference `secretKeyRef: {name: -wandb-api-key, key: WANDB_API_KEY}` on the training head container. - ---- - -## 6. Node pool — labels, taints, GPU resources - -RayCluster specs pin workers via `nodeSelector` + `tolerations`. The -bundled examples use two keys that are NVIDIA-internal (`gpu-wrangler.nvidia.com/lease` -and `platform.nvidia.com/gpu`); you'll almost certainly want to swap -those for whatever your GPU node pool advertises. - -```bash -# Inspect an existing GPU node: -kubectl get node -o jsonpath='{.metadata.labels}' | python3 -m json.tool -kubectl get node -o jsonpath='{.spec.taints}' | python3 -m json.tool -``` - -Then update `clusters.training.spec.*.template.spec.nodeSelector` -and `.tolerations` in your recipe's `.infra.yaml` to match. The -examples use YAML anchors (`&shared_node_selector`) so the selector -lives in one place per file. - -The GPU worker's resource request must include: - -```yaml -resources: - limits: - cpu: "176" # leave headroom for daemonsets - memory: "1800Gi" # ditto - nvidia.com/gpu: "8" # must equal rayStartParams.num-gpus - vpc.amazonaws.com/efa: "32" # AWS p5 only; drop on other clouds -``` - -On AWS EKS with p5 instances you also need the EFA device plugin -installed in the cluster (`https://github.com/aws/eks-charts/tree/main/stable/aws-efa-k8s-device-plugin`). -On non-AWS clouds, remove the EFA line entirely. - ---- - -## 7. Sanity-check with `nrl-k8s` - -Install the CLI if you haven't already: - -```bash -uv tool install ./tools/nrl_k8s -# or for editable dev: cd tools/nrl_k8s && uv pip install -e ".[test]" -nrl-k8s --version -``` - -Load-test a recipe without hitting the API server: - -```bash -nrl-k8s check \ - tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml -``` - -This validates schema, resolves the full infra, and prints a -one-page summary. No cluster calls. - ---- - -## 8. Bring up a RayCluster - -```bash -nrl-k8s cluster up \ - tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ - --role training -``` - -The command applies the RayCluster CR and waits for -`.status.state == ready`. Typical times: 1–2 min image pull + -scheduling, then steady-state. - -Check the head pod came up: - -```bash -nrl-k8s cluster list -# → raycluster-single-qwen3-4b ready -kubectl get pods -n $NS \ - -l ray.io/cluster=raycluster-single-qwen3-4b -# → both head + worker showing Running -``` - ---- - -## 9. Browse the Ray dashboard - -```bash -nrl-k8s cluster dashboard raycluster-single-qwen3-4b -``` - -Port-forwards `localhost:8265` and opens your browser. First run -auto-fixes a known uv-symlink issue on the head pod (~30 s one-time -reinstall of `ray[default]` in copy mode). Pass `--no-fix` if your -image was built with `ENV UV_LINK_MODE=copy` already. - -If the dashboard still renders blank, see the "Blank dashboard" -section in `README.md` — the permanent fix is in the image build. - ---- - -## 10. Submit a dry run - -Pick a mode up front: - -- **interactive** (default): laptop stays attached, tails logs, exits - on terminal state. Good for "does my recipe even start." Uploads a - `working_dir` via Ray's SDK (100 MiB cap). -- **batch**: `kubectl exec` + `nohup` on the training head pod. Code - must already be on disk in the pod (`launch.codeSource=image` or - `lustre`). Returns in <30 s and the laptop can disconnect. Use this - for real production runs. - -Dev iteration: - -```bash -nrl-k8s run \ - tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ - --mode interactive grpo.max_num_steps=2 -``` - -Production (using the prod-mode variant of the gym-disagg example): - -```bash -nrl-k8s launch \ - tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ - --run-id smoke-$(date +%s) -``` - -Observability is transport-aware: - -```bash -nrl-k8s job logs --infra --role training -f -nrl-k8s job stop --infra --role training -``` - ---- - -## 11. Clean up - -```bash -# Stop a specific run: -nrl-k8s job stop --infra --role training - -# Tear down the RayCluster when you're done: -nrl-k8s cluster down --infra --role training -# or by name: -nrl-k8s cluster down --name raycluster-single-qwen3-4b -``` - ---- - -## Per-cluster fixture summary - -| Thing | Where it lives | Source of truth | -|---|---|---| -| KubeRay operator | `kuberay-system` namespace | `infra/helm/helmfile.yaml` | -| EFA / GPU device plugins | `kube-system` namespace | `infra/helm/` | -| Your working namespace | cluster root | `kubectl create namespace` | -| `nemo-rl-endpoint-registry` SA + Role | per-namespace | RBAC snippet in §3 | -| `ngc-registry-secret` pull secret | per-namespace | `kubectl create secret docker-registry` | -| `wandb-api-key` secret | per-namespace | `kubectl create secret generic` | -| Node pool selectors / tolerations | per-cluster node labels | update `.infra.yaml` `_shared` anchors | - -The `.infra.yaml` file is the single place per recipe where -cluster-specific values live. Once you've adapted the examples to -your cluster, commit them somewhere your team can share (a team -`infra/` repo, or the `nrl_k8s/examples/` dir if you contribute -back). diff --git a/tools/nrl_k8s/docs/production-readiness.md b/tools/nrl_k8s/docs/production-readiness.md deleted file mode 100644 index 42fb73b1705..00000000000 --- a/tools/nrl_k8s/docs/production-readiness.md +++ /dev/null @@ -1,134 +0,0 @@ -# nrl-k8s — Production Readiness Audit - -Scope: read-only audit of `tools/nrl_k8s/` at commit `hemil/k8s-infra`. -The CLI is end-to-end functional on Qwen3-4B disagg and single-cluster runs, and 48 unit tests pass on `config`, `schema`, `manifest`. What follows is a prioritised backlog of gaps that block or jeopardise a production launch. - -Priority rubric: **P0** = blocks production deploy / data-loss / security; **P1** = ship-blocker for v1.0 (usability, reliability under failure); **P2** = post-1.0 polish. - -Legend: each finding lists `path:line -> issue -> fix -> priority -> est. minutes`. - ---- - -## 1. Operational robustness - -The CLI talks to k8s and the Ray job submission API over the network and through a `kubectl port-forward` subprocess, but very little of it is defensive against a flaky network, a re-authenticated kubeconfig, or a dead port-forward. - -- `submit.py:41-82 (dashboard_url)` -> port-forward is launched once and never re-established. A laptop Wi-Fi blip, VPN reconnect, or a kubelet restart kills the forward mid-submit and the Ray SDK call fails with an opaque `ConnectionError` -> wrap the dashboard interaction in a reconnect loop (kill + respawn port-forward, wait for TCP, retry the SDK call on `urllib3.exceptions.NewConnectionError`/`MaxRetryError`) -> **P1** -> 60 -- `submit.py:68-73` -> port-forward's stdout is pipe-buffered and only read after the proc exits (`_wait_for_tcp` only reads on early death), so a kubectl that prints "Handling connection for N" but never establishes the forward looks like "timed out" with no diagnostic -> drain stdout into a rolling buffer in a daemon thread and include last N lines in the `TimeoutError` -> **P1** -> 30 -- `submit.py:60-66` -> `kubectl port-forward` is invoked without `--address 127.0.0.1` or `--context`. On a multi-context kubeconfig (common at NVIDIA) we may forward against the wrong cluster -> pass `--context=$(current-context)` and `--address=127.0.0.1` explicitly; surface the chosen context in log -> **P1** -> 15 -- `k8s.py:116-135 (wait_for_raycluster_ready)` -> polls with a flat `poll_s=5` without exponential backoff and without tolerating transient API errors: a single `ApiException` (500, 503, SSL timeout) propagates out and aborts `run`. Likelihood: high during cluster-autoscaler events -> wrap `get_raycluster` in a retry helper that swallows 429/5xx/`urllib3.exceptions.ProtocolError` up to N tries before re-raising -> **P0** -> 45 -- `k8s.py:28-34 (load_kubeconfig)` -> `@functools.cache`d. If the kubeconfig token expires mid-run (AWS SSO, short-lived OIDC), subsequent API calls fail and there is no re-load path -> drop the cache on 401; or re-call `load_kube_config` on each `ApiException.status == 401` -> **P1** -> 45 -- `k8s.py:46-69 (apply_raycluster)` -> on 409 it unconditionally `PATCH`es even if another user's run owns the RayCluster. There is no owner-label check, no `resourceVersion` race guarantee; two concurrent `run`s can silently overwrite each other's specs -> before patch, verify a `managed-by=nrl-k8s` label and the current user / run-id; error out if the existing cluster was not ours -> **P0** -> 60 -- `orchestrate.py:108, 295, inspect.py:120` -> bare `except Exception` swallow every failure including `KeyboardInterrupt`'s non-`BaseException` cousins and API auth failures; user sees `daemon_status=None` and assumes "not running" when in truth the dashboard is unreachable -> narrow to `(ApiException, JobSubmissionClientError, ConnectionError, TimeoutError)` and surface the reason in the status row -> **P1** -> 30 -- `orchestrate.py:304-316 (_wait_for_http)` -> health-check loop retries every 5s with no backoff; a misconfigured URL (e.g. cluster-internal DNS when the CLI is running on a laptop) silently spins for 5 min -> detect obvious laptop-cannot-reach-cluster-DNS conditions (`socket.gaierror` on `*.svc.cluster.local`) and fail-fast with an actionable message -> **P1** -> 20 -- `orchestrate.py:282-301 (_wait_job_stopped)` -> a Ray Job that Ray insists is `RUNNING` but whose node has been evicted stays `RUNNING` forever; timeout is just 60s and then the loop logs "continuing" and proceeds to submit a clashing daemon -> after timeout, call `client.delete_job` (and error if it still isn't terminal) instead of silently racing -> **P1** -> 30 -- `submit.py:121-146 (tail_job_logs)` -> daemon thread uses a blocking `q.get()` with no timeout, so `Ctrl+C` during a tail will cancel the main thread's sleep but leave the async asyncio loop hanging until the process exits. Usually benign but leaks file descriptors under long-running observability sessions -> use `q.get(timeout=1)` in a loop and wind down the asyncio loop cleanly -> **P2** -> 20 -- `cli.py:82/170/227/432/536/602/619/656` -> every top-level error handler prints `error: {exc}` and calls `sys.exit(1)`. The raw exception from `kubernetes.client.ApiException` includes a multi-line dump with headers, which is noisy; the root cause (e.g. "kubeconfig expired") is buried -> classify exceptions and emit a short, actionable first line (`hint: kubeconfig expired; run aws sso login`) before the full trace under `-v` -> **P1** -> 60 -- `submit.py:179-190 (_wait_for_tcp)` -> polls every 0.5s for 30s; if kubectl spawns but the LB hasn't propagated, you see "didn't open 127.0.0.1:X in 30s" with no hint. Make the timeout configurable via `infra.submit.portForwardTimeoutS` -> **P2** -> 15 -- `orchestrate.py:218-225 (run)` -> clusters are brought up sequentially (generation, then gym, then training). A transient failure on the gym cluster tears the whole run; there is no resumability (the CLI doesn't persist a run-state) -> add a local state file (`~/.cache/nrl-k8s/runs/.json`) that tracks which clusters are up and allows `--resume` -> **P1** -> 120 - -## 2. Secrets handling - -The CLI doesn't explicitly read secrets, but it emits many code paths that dump user-provided YAML, env dicts, or pod manifests. Those payloads frequently contain tokens. - -- `cli.py:86-91 (validate)` -> the resolved `InfraConfig` is printed verbatim. The schema allows `launch.env` / `daemon.env` / `networking.extra_env` as `dict[str,str]`, so any user that puts `{WANDB_API_KEY: xxx}` into the recipe sees it leak into the terminal and into `nrl-k8s validate > out.yaml` commits -> either deny-list known secret-looking keys (`*_API_KEY`, `*_TOKEN`, `*_PASSWORD`) when printing, or redact by default and add `--show-secrets` -> **P0** -> 30 -- `cli.py:117-126 (plan)` -> prints the full RayCluster manifest including any env with `valueFrom.secretKeyRef`. That's only a name-reference and usually fine, but if a user inlined a plaintext secret into `spec.headGroupSpec.template.spec.containers[*].env` it gets dumped. Same redaction pass should apply here -> **P1** -> 15 -- `orchestrate.py:126-148` -> on `--replace` the log emits the daemon submission id but not the env; still, the `submit_ray_job` call sends `env_vars=daemon.env` which travels in cleartext to the Ray dashboard (HTTP, no TLS by default). When the dashboard is behind a LoadBalancer this is a genuine risk -> document the HTTP-vs-HTTPS boundary in README + SECURITY.md; add a `submit.insecureHttpDashboard: bool` guard that refuses non-localhost, non-TLS dashboards for env-bearing jobs -> **P0** -> 60 -- `cli.py:82 / 170 / 227` -> `error: {exc}` stack traces leak kubeconfig auth tokens in some `ApiException` message bodies (`kubernetes` client includes response body by default in `reason`). A traceback on a 401 from an OIDC-authenticated cluster typically includes the bearer token in the `Authorization` header that the client echoes back -> before echoing an `ApiException`, scrub `Authorization:` / `Bearer ` patterns from `exc.body` and `exc.headers` -> **P0** -> 40 -- `workdir.py:53-92 (stage_workdir)` -> no scrubbing of `.env`, `*.pem`, `*.key`, `credentials*`, `id_rsa*`, `secrets.yaml` from the copied tree. If a researcher `cd`s into a repo root that contains a `.env` it is uploaded to GCS as part of the working_dir zip and served from the Ray dashboard -> add those patterns to `_IGNORE_PATTERNS`; add a pre-upload size+names preview (`Uploading 97MB across 12,437 files; first hit: examples/.env`) with opt-in `--yes` -> **P0** -> 30 -- `inspect.py:57-66 (collect_status) / cli.py:263-276 (status)` -> not a direct leak but the status command attaches via `dashboard_url` to three clusters in series, and any port-forward failure logs the `kubectl port-forward` stderr which contains the full kubeconfig path and context. Not a secret by itself, but combined with AWS SSO caches it's fingerprinting data -> redact the kubeconfig path from errors -> **P2** -> 15 - -## 3. Test coverage holes - -Only 3 modules have unit tests (`config`, `schema`, `manifest`, totalling 48 tests). The CLI's orchestration, I/O, and CLI layer are wholly untested. - -- `orchestrate.py` -> zero tests. The highest-value business logic (replace logic, `_infer_disagg_job_id`, ConfigMap reset, sequential cluster bring-up, `_wait_for_http`) runs only in production -> add pytest-mock tests that fake `k8s.*` and `JobSubmissionClient` and cover: daemon skipped when RUNNING without --replace; daemon re-submitted with fresh id when --replace; FAILED daemon errors without --replace; `_wait_for_http` returns on 500 **P0** -> 180 -- `submit.py` -> zero tests. The port-forward lifecycle + `submit_ray_job` are the CLI's only k8s-to-Ray bridge -> add subprocess-mocked tests for `dashboard_url` (in-cluster vs laptop branch, kubectl-missing, early-exit, timeout); add a test that `submit_ray_job` assembles the correct `runtime_env` when `pip=None`, `env_vars=None`, `submission_id=None` -> **P0** -> 90 -- `workdir.py` -> zero tests, but it silently drops `.gitignore` files (a bug-fix encoded as behaviour) and implicitly relies on `.gitignore` stripping to ship training data; **no test pins this invariant** -> add tests: `.gitignore` is dropped; a `data/foo.jsonl` file under a `.gitignore`d path is still present in staging; `extra_files` path collisions are well-defined; missing optional paths are skipped -> **P1** -> 45 -- `inspect.py` -> zero tests. `_latest_daemon_job` branch that strips the timestamp suffix is fragile and untested -> add tests mocking `JobSubmissionClient.list_jobs` with base/suffixed/unrelated ids -> **P1** -> 45 -- `k8s.py` -> zero tests. `apply_raycluster` 409 path and `wait_for_raycluster_ready` timeout path never exercised -> pytest-mock the `CustomObjectsApi` and assert both branches -> **P1** -> 60 -- `cli.py` -> zero tests. Nobody has exercised, e.g., `cluster down --name` vs `--role`, or the invariant that `--infra` and recipe-level `infra:` conflict -> use click's `CliRunner` to exercise each subcommand's argument validation and the `sys.exit(2)` path (without actually touching k8s) -> **P1** -> 120 -- `config.py` -> coverage gap: override partitioning for `+infra.foo=x` / `~infra.foo` (append/remove) is implemented but not tested -> add test cases for `+infra.scheduler.queue=x` and `~infra.scheduler.queue` semantics -> **P2** -> 20 -- `schema.py` -> coverage gap: `LaunchSpec.entrypoint` is `Optional` so the sentinel "must be set for nrl-k8s launch" is enforced only at runtime in `orchestrate.submit_training`. Add a schema-level test that at least one scheduler/queue combo in an attach-mode recipe is accepted, and that `launch.mode=attach` without any of the three attach targets is rejected (exists but no test on mixing with `launch.entrypoint=None`) -> **P2** -> 20 - -## 4. CLI UX gaps - -- `cli.py:237-240 / 351-355 / 365-374` -> `doctor`, `dashboard`, `dev up`, `dev down` are advertised in `--help` but just print `not yet implemented (phase: N)` and exit with 2. This is actively misleading for a v1.0 release -> either hide them behind a `NRL_K8S_SHOW_STUBS=1` env flag, or remove them from the group until implemented -> **P1** -> 15 -- `cli.py:82, 170, 227` -> bare stack traces go to stderr. No `--verbose`/`--quiet`/`-v` flag, no log format toggle -> add `--log-level` at the root group, default INFO, with DEBUG surfacing full tracebacks. Use `logging` instead of `click.echo` for diagnostics -> **P1** -> 60 -- no `--dry-run` anywhere -> `nrl-k8s run --dry-run` should plan all three manifests, print what daemons would be submitted with what entrypoints, and exit 0. This is critical for reviewers who can't actually `kubectl apply` -> **P0** -> 60 -- `submit.py / orchestrate.py` -> no progress indicator on 97 MB working_dir uploads or on the 2-3 min cluster bring-up; users ctrl+C thinking the CLI is hung -> print periodic "still waiting on RayCluster X (elapsed 90s / state=Pending)" lines every 30s in `wait_for_raycluster_ready`; print byte count before calling `submit_job` -> **P1** -> 45 -- `cli.py:150-177 (launch) / 201-234 (run)` -> `--follow` tails training logs, but there is no way to tail a daemon at submit time. When gym fails to come up, the user has to find out post-hoc via `nrl-k8s logs --role gym`. Add `--follow-daemons` that streams all daemon logs in parallel -> **P2** -> 60 -- `cli.py:484-497 (cluster list)` -> the `--namespace` flag ignores recipe; other subcommands all require a recipe for namespace resolution. Inconsistent. Either accept an optional recipe or keep it strictly `--namespace` and document -> **P2** -> 10 -- `cli.py:107-126 (plan)` + `cli.py:65-95 (validate)` -> no `--output` flag to write to a file (users have been piping to `> out.yaml`, which breaks `--show-recipe` two-section output) -> add `-o/--output PATH` -> **P2** -> 15 -- no shell completion (`click`'s `click.shell_completion`) -> a trivial 10-line addition; adds noticeable polish -> **P2** -> 15 -- error hint when `--infra` file and recipe both declare `infra:` at `config.py:202-208` -> the ValueError is fine; surfaces through `cli.py:620`. Consider telling the user which line of the recipe has `infra:` -> **P2** -> 15 - -## 5. Packaging + versioning - -- `pyproject.toml` -> dependencies list open-ended lower bounds but no upper pin; `ray[default]>=2.52` will happily install 3.x breaking changes. Pin to a tested range (`>=2.52,<3`) and bump deliberately -> **P1** -> 15 -- `pyproject.toml` -> no `[project.urls]` (home, issues, changelog). No `classifiers`. No `keywords` -> **P2** -> 10 -- no `CHANGELOG.md` -> add a keep-a-changelog with the current 0.1.0 feature set documented -> **P1** -> 20 -- `__init__.py:3` -> `__version__ = "0.1.0"` is duplicated from `pyproject.toml`. Moving to `importlib.metadata.version("nrl-k8s")` avoids drift -> **P2** -> 10 -- no CI for the package. `tools/nrl_k8s` is not wired into the repo's `.github/workflows/*` that I can see -> add a minimal GHA job that runs `pip install tools/nrl_k8s[test]` and `pytest tools/nrl_k8s/tests/unit` on PRs touching the tool -> **P0** -> 45 -- no entry-point test (`pip install . && nrl-k8s --version`) in CI -> add a `tests/unit/test_entry_point.py` that uses `CliRunner` to invoke `--version` and `--help` -> **P1** -> 15 -- no license headers on source files; pyproject says Apache-2.0 but the Python files have no SPDX line. Repo convention check needed -> **P2** -> 15 -- `orchestration/`, `schedulers/`, `backends/`, `templates/` are empty directories with empty `__init__.py` files. Either populate or remove -> **P2** -> 10 - -## 6. Config validation gaps - -The schema is strict (`extra=forbid`), but several runtime invariants are not expressed in pydantic and blow up downstream only after you bring up the clusters. - -- `schema.py:282-302 (ClusterSpec)` -> `spec` is `dict[str, Any]` — zero validation on the RayCluster body. Missing `headGroupSpec` or containers without a name yield cryptic KubeRay webhook errors only after `nrl-k8s run` applies -> add a light check in `manifest.py` that walks the body and raises a clean error for common mistakes (no containers, no head group, image mentioned but empty string) before the k8s call -> **P1** -> 45 -- `orchestrate.py:228-243 (_infer_disagg_job_id)` -> the invariant that training's `+env.disagg_job_id=` matches gym's `--job-id ` is enforced only by a regex hack on the gym entrypoint and with a best-effort "if we can parse it, else skip ConfigMap delete". A mismatch silently makes gym hang forever because training publishes to a different ConfigMap -> promote `disagg_job_id` to a first-class schema field (`infra.launch.disaggJobId: str | None`) and inject it as an env var into both the gym daemon and the training entrypoint; emit a validator error if gym declares one and training is missing it -> **P0** -> 75 -- `schema.py:192-224 (LaunchSpec/AttachSpec)` -> `launch.attach.training: str | None` vs. `clusters.training.name: str` are both strings and nothing enforces they reference the same RayCluster. Similar for `attach.generation` / `clusters.generation.name`. If they drift, the CLI applies a new RayCluster under `clusters.training.name` but submits the job against a nonexistent cluster name in `attach.training` -> add a `model_validator` on `InfraConfig` that, in `mode=attach`, enforces `attach.` equals `clusters..name` (or is None when the cluster is None) -> **P0** -> 30 -- `schema.py:197-207 (LaunchSpec.entrypoint)` -> the doc-comment says "required for `nrl-k8s launch` / `nrl-k8s run`" but the schema marks it Optional and `orchestrate.submit_training:164-165` throws a runtime ValueError. Users who pass only `cluster up --role generation` legitimately need no entrypoint; but `nrl-k8s run` without an entrypoint should fail at load time -> add a `post-validate` step in `_load_or_exit` (or make it a top-level `run`/`launch` precondition in the CLI) that checks `launch.entrypoint` is non-empty and that `clusters.training` is defined -> **P1** -> 30 -- `schema.py:259-279 (DaemonSpec)` -> `submissionId` is optional; but `orchestrate.submit_daemon:106-107` does `client.get_job_status(daemon.submissionId)` unguarded if it's None, which will raise `TypeError`. Add `@model_validator` to require `submissionId` when parent `launch.peerWatcher=True` or when `nrl-k8s run --replace` is likely -> **P1** -> 20 -- `schema.py:193-195 (AttachSpec)` -> gym may be declared without a training cluster; but `nrl-k8s run` assumes `training` exists. Schema doesn't forbid gym-only runs -> either allow gym-only (document) or validate -> **P2** -> 15 -- `config.py:216-218` -> underscore-prefixed top-level keys (e.g. `_shared: &anchors`) are stripped. Silent. A typo like `_shred` would be silently dropped instead of flagged -> log-warn on strip in debug mode -> **P2** -> 10 -- `schema.py:82` -> `NetworkingSpec.extra_env` is never applied anywhere (grep confirms). Dead config key -> either wire it into manifest/patch or remove -> **P2** -> 20 -- `schema.py:228-246 (ResourcesSpec)` -> declared but never consumed — `manifest.py` does not read it. Another dead knob that will mislead users -> remove or implement before v1.0 -> **P1** -> 30 - -## 7. Multi-environment portability - -The code itself is surprisingly free of AWS-specific strings, but the examples (and by extension the demo path the user follows) bake in assumptions. - -- `examples/qwen3_4b_if_full_disagg.infra.yaml:42, 49, 104-105` -> `vpc.amazonaws.com/efa`, `enp71s0`, `FI_PROVIDER=efa` hardcoded in the only examples. First-run on Azure/GCP/on-prem will fail silently (NCCL falls back to TCP and training slows 100x) with no error -> add a second example (`examples/azure_*.infra.yaml` or `examples/no_efa.infra.yaml`) that doesn't assume EFA, plus a note in README explaining the EFA case -> **P1** -> 45 -- `examples/qwen3_4b_if_full_disagg.infra.yaml:14-17` -> `gpu-wrangler.nvidia.com/lease: nemo-rl-testing` label is NVIDIA-internal. Reused in node-selector + toleration -> move to a top-level alias (`${node.lease}`) and document it as "replace with your own scheduler's pool label" -> **P2** -> 20 -- `schema.py:32-35 (SchedulerKind)` -> enum includes `kai`, `kueue`, `default` but nothing in `manifest.py` or `orchestrate.py` actually reads `infra.scheduler.kind` beyond validation. Researchers set `kind: kai, queue: priority-team` and expect the CLI to patch the `scheduling.run.ai/queue` label onto pods — but it doesn't -> either auto-patch the KAI `scheduling.run.ai/queue` / Kueue `kueue.x-k8s.io/queue-name` labels onto `spec.headGroupSpec.template.metadata.labels`, or document loudly that users must add the label themselves -> **P0** -> 60 -- `workdir.py:33-46 (DEFAULT_RAY_UPLOAD_PATHS)` -> the defaults are NeMo-RL-monorepo-shaped (`3rdparty/Gym-workspace/Gym/...`). A thinned-down NeMo-RL checkout will miss most of these. The `if not src.exists(): continue` at workdir.py:79 makes it silent -> log a warning when a default path is skipped; consider making the default list empty and forcing recipes to declare their paths -> **P1** -> 20 -- `orchestrate.py:252 (cm_name = f"nemo-rl-endpoints-{job_id}")` -> the ConfigMap name prefix is hard-coded to match the server-side code in `nemo_rl.distributed.k8s_endpoint_registry`. If the server-side code ever changes the prefix, `--replace` quietly stops working. Share the prefix in a single source of truth (import from `nemo_rl.distributed.k8s_endpoint_registry` with a fallback for the standalone install path) -> **P1** -> 15 -- `submit.py:29 (DASHBOARD_PORT = 8265)` -> hardcoded. KubeRay convention, fine; but a bespoke operator with `--dashboard-port` different from 8265 can't use the CLI -> plumb through `infra.submit.dashboardPort` -> **P2** -> 15 - -## 8. Concurrency safety - -Two researchers running `nrl-k8s run` or even `status` against the same namespace concurrently is a first-class use case (shared dev namespace is standard), and several paths are not safe. - -- `submit.py:173-176 (_free_port)` -> picks a random free port at `bind(0)`. Two concurrent invocations may race: both get port P, the first `kubectl port-forward` binds, the second sees `bind: address already in use` -> either retry with a fresh port on `OSError: EADDRINUSE` from the port-forward, or keep the socket bound while spawning kubectl and hand off; prefer the retry because `SO_REUSEADDR` doesn't help with kubectl -> **P1** -> 20 -- `orchestrate.py:246-254 (_reset_endpoint_registry)` -> deletes the ConfigMap unconditionally. If User A is running with job-id `foo` and User B does `run --replace` with the same `foo`, User A's training loses its registry mid-run and rendezvous breaks silently -> scope ConfigMap name by user (`nemo-rl-endpoints--`) or add an owner annotation and refuse delete if the annotation doesn't match the current run -> **P0** -> 45 -- `orchestrate.py:71 (apply_raycluster)` + `k8s.py:46-69` -> concurrent applies of the same RayCluster name clobber each other; one wins the patch and the other's topology is lost. See §1 above; same fix (owner-label + resourceVersion) -> **P0** -> (shared) -- `submit.py:103-109 (submit_ray_job)` -> `submission_id` is user-supplied (for daemons); Ray's server rejects a re-used id, which is nice, but two concurrent `run --replace` both compute `_fresh_submission_id` from `time.time()` and can collide at second granularity -> use `time.time_ns()` or append a short random suffix -> **P1** -> 10 -- `workdir.py:74 (mkdtemp)` -> each invocation gets its own dir, so staging is fine; but nothing cleans up old `nrl-k8s-workdir-*` directories. Over weeks of use `/tmp` gets hundreds of 100 MB copies -> add a GC helper invoked at CLI startup that deletes `nrl-k8s-workdir-*` older than 24h -> **P2** -> 20 -- `orchestrate.py:183-194 (submit_training --replace)` -> stops ALL running Ray jobs on the training cluster, not just ones owned by this recipe. If a second user shares the training cluster, their job gets killed by your `--replace` -> filter by `metadata.submission_id` prefix (a per-recipe unique prefix) or by a runtime_env label -> **P0** -> 30 - ---- - -## Top 10 to do first - -1. **P0, 30m** — `§2` Scrub `Authorization: Bearer ...` + redact `*_API_KEY` from error output and `validate` output (`cli.py:82/170`, `cli.py:86-91`, `submit.py:179`). -2. **P0, 30m** — `§2` Add `.env`, `*.pem`, `*.key`, `credentials*`, `id_rsa*` to `_IGNORE_PATTERNS` in `workdir.py:19` and preview the first few staged paths before upload. -3. **P0, 60m** — `§7` / `§5` Auto-patch `scheduling.run.ai/queue` (KAI) / `kueue.x-k8s.io/queue-name` (Kueue) labels onto pod templates when `infra.scheduler.kind` is set — currently the enum exists but is never applied. -4. **P0, 75m** — `§6` Promote `disagg_job_id` to a first-class schema field and inject it into both the gym daemon and training entrypoint; forbid recipes whose gym + training job-ids differ. -5. **P0, 60m** — `§1` Wrap every k8s API call (`get_raycluster`, `list_rayclusters`, `list_namespaced_pod`) in a retry helper that swallows 429/5xx/`ProtocolError` for ~3 tries with exponential backoff. -6. **P0, 60m** — `§1` Owner-label guard in `apply_raycluster`: refuse to patch a RayCluster that lacks `managed-by=nrl-k8s` or whose `nrl-k8s/run-id` differs from the current invocation. -7. **P0, 45m** — `§8` Scope endpoint-registry ConfigMap by user (`nemo-rl-endpoints--`) so concurrent `run --replace` can't trash each other's rendezvous. -8. **P0, 60m** — `§4` `--dry-run` on `run` / `launch` / `cluster up`: render + print all manifests and daemon/entrypoint commands without touching k8s. -9. **P0, 180m** — `§3` Tests for `orchestrate.py` covering `submit_daemon --replace`, `--no-replace` FAILED handling, and `_reset_endpoint_registry`. -10. **P0, 45m** — `§5` Add a GitHub Actions job that runs `pip install tools/nrl_k8s[test] && pytest tools/nrl_k8s/tests/unit` on every PR touching the tool, plus a smoke-test entry-point check. - -## Running totals by priority - -- P0 findings: 14 (est. 775 min / 12.9 h) -- P1 findings: 31 (est. 1305 min / 21.75 h) -- P2 findings: 17 (est. 305 min / 5.1 h) - -Overall, ~39 engineering hours to reach a credible v1.0. The P0 list alone is under 2 working days and closes every data-loss / secret-leak / cross-user safety gap that the current codebase has. diff --git a/tools/nrl_k8s/docs/recipes.md b/tools/nrl_k8s/docs/recipes.md deleted file mode 100644 index 0a2bfbc9381..00000000000 --- a/tools/nrl_k8s/docs/recipes.md +++ /dev/null @@ -1,419 +0,0 @@ -# Writing `nrl-k8s` recipes - -A `nrl-k8s` run is two YAML files. Together they tell the CLI -*what* to train (the recipe) and *where* to run it (the infra). This guide -covers the split, every infra field, and how to port a recipe from one -cluster to another. For the CLI command surface, see the README. - -## Recipe vs. infra - -### Recipe file (`.yaml`) - -Pure NeMo-RL config — exactly what a training entrypoint like -`examples/nemo_gym/run_grpo_nemo_gym.py` expects. Typical keys: `cluster`, -`policy`, `grpo`, `data`, `logger`, `checkpointing`. Inherits from a -parent recipe via `defaults:`. Nothing K8s-specific lives here. - -```yaml -# tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml -defaults: ../../../examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml - -cluster: - gpus_per_node: 8 - num_nodes: 1 -grpo: - num_prompts_per_step: 32 - max_num_steps: 200 -policy: - train_global_batch_size: 512 - max_total_sequence_length: 16384 -``` - -The CLI stages the *merged* recipe (after `defaults:` resolution) as -`nrl_k8s_run.yaml` at the `working_dir` root before submitting the job, so -the entrypoint can reference it by the constant name -(`--config nrl_k8s_run.yaml`). - -### Infra file (`.infra.yaml`) - -K8s-only — the pydantic `InfraConfig` body, defined in -`tools/nrl_k8s/src/nrl_k8s/schema.py`. The recipe and infra files are -loaded independently and merged by the CLI. - -```yaml -# tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml (excerpt) -namespace: nemo-rl-testing -image: nvcr.io/nvidian/nemo-rl:nightly -imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] -serviceAccount: nemo-rl-endpoint-registry -launch: - mode: attach - ... -clusters: - generation: { ... } - gym: { ... } - training: { ... } -``` - -You can also put infra into the recipe as a top-level `infra:` key and omit -`--infra`. Don't do both — the loader refuses -(`tools/nrl_k8s/src/nrl_k8s/config.py:202`). - -## Infra fields - -Every field in the sections below corresponds to a pydantic model in -`schema.py`. The source is the authoritative reference; what follows is -grouped by concern with one example per area. - -### Namespace, image, pull secrets - -```yaml -namespace: nemo-rl-testing # required -image: nvcr.io/nvidian/nemo-rl:nightly # required; patched onto every container -imagePullSecrets: [my-registry-secret] # attached to every pod template -rayVersion: "2.52.0" # optional — defaults to the image default -serviceAccount: nemo-rl-endpoint-registry # set per pod when non-null -labels: {team: nemo-rl} # merged into every RayCluster's metadata -annotations: {} -``` - -`image` is the one field you'll change most often when moving between -clusters. `serviceAccount` is required when anything in the run talks to the -Kubernetes API (e.g. `K8sEndpointRegistry` publishing into a ConfigMap, -used by gym/training rendezvous). - -### Scheduler - -```yaml -scheduler: - kind: kai # "kai" | "kueue" | "default" - queue: priority-team # required when kind != "default" -``` - -`kai`/`kueue` trigger a scheduler-specific annotation/label patch on each -RayCluster. `default` leaves the Kubernetes default scheduler alone. - -### Placement - -```yaml -placement: - nodeSelector: - gpu-wrangler.nvidia.com/lease: nemo-rl-testing - tolerations: - - {key: platform.nvidia.com/gpu, operator: Equal, value: "true", effect: NoSchedule} - affinity: null # rare — raw-dict passthrough to pod spec -``` - -Node selectors and tolerations here apply to every pod template; the -examples duplicate them via YAML anchors inside each `clusters..spec` -for per-cluster flexibility. - -### Networking - -```yaml -networking: - hostNetwork: false # see note under the worked example below - gloo_socket_ifname: enp71s0 - nccl_socket_ifname: enp71s0 - nccl_ib_disable: false - nccl_net: OFI # "Socket" | "IB" | "OFI" - extra_env: {FI_PROVIDER: efa} -``` - -These become pod-level env vars on containers the CLI templates. Most real -recipes leave `networking:` at its defaults and bake the env into the -container image instead — setting them in pod env *and* `launch.env` at the -same time triggers Ray's runtime-env merge conflict (see the inline comments -in `qwen3_4b_if_full_disagg.infra.yaml`). - -### Workspace, HF cache, checkpoints - -```yaml -workspace: - kind: rayUpload # "lustre" | "pvc" | "hostPath" | "rayUpload" | "auto" - pvcName: null # required when kind in {lustre, pvc} - mountPath: /mnt/nemo-rl - repoSubdir: workdirs - size: null # only used when kind=lustre and PVC needs creating - hostPath: null # kind=hostPath only (dev/kind only) -hf_cache: - kind: none # "lustre" | "pvc" | "emptyDir" | "none" - pvcName: null - mountPath: /root/.cache/huggingface -checkpoints: - kind: none # "lustre" | "pvc" | "none" - pvcName: null - mountPath: /mnt/nemo-rl/checkpoints -``` - -`workspace.kind=rayUpload` is the default and the one the shipped examples -use — the CLI packages code into a tmpdir and Ray uploads it to the -cluster's GCS. PVC-backed kinds exist for larger repos or when you want -checkpoints to persist beyond the RayCluster lifetime; today they're -wired at the manifest level but not exercised by the example recipes. - -### Submit (how the CLI gets a job in) - -```yaml -submit: - kind: sdk # "sdk" (Ray Job SDK) | "rayjob" (RayJob CRD) - portForward: auto # "kubectl-ray-plugin" | "kubectl-port-forward" | "auto" - devPod: auto # "auto" | "required" | "skip" (not yet wired) - localDashboardPort: 18265 # avoids collision with `kubectl-ray session` -``` - -### Launch - -```yaml -launch: - mode: attach # "single" | "rayjob" | "attach" | "bringup" - attach: - generation: raycluster-generation-qwen3-4b - gym: raycluster-gym-qwen3-4b - training: raycluster-rl-qwen3-4b - peerWatcher: false # inject the peer-watcher sidecar for failure cascades - entrypoint: | # the training command; see below - export ... - python -u examples/nemo_gym/run_grpo_nemo_gym.py --config nrl_k8s_run.yaml ... - env: {} # runtime_env.env_vars for the training job - rayUploadPaths: # repo-relative paths to include in working_dir - - nemo_rl - - examples - - infra/examples - - 3rdparty/Gym-workspace/Gym/nemo_gym - - ... -``` - -`entrypoint` is required for `launch` / `run`. The CLI stages the merged -recipe as `nrl_k8s_run.yaml` at the `working_dir` root; the entrypoint -references it by that fixed name. - -Keep `rayUploadPaths` narrow — Ray's Job SDK caps the upload at 100 MiB. -List individual files inside heavy directories (see the example gym config) -when a full tree would include unneeded data. `null` means "use the built-in -default" from `tools/nrl_k8s/src/nrl_k8s/workdir.py`. - -### Clusters and daemons - -```yaml -clusters: - generation: - name: raycluster-generation-qwen3-4b - labels: {disagg.nemo-rl/cluster: generation-qwen3-4b} - annotations: {} - daemon: - submissionId: qwen3-4b-generation-server-v7 - entrypoint: | - python -u examples/run_standalone_generation_server.py ... - env: {} # runtime_env.env_vars for the daemon job - healthCheckUrl: null # optional — CLI polls before returning - healthCheckTimeoutS: 300 - rayUploadPaths: [nemo_rl, examples, infra/examples] - spec: - # Full RayCluster .spec — headGroupSpec, workerGroupSpecs, etc. - # Free-form dict; no pydantic schema. - rayVersion: "2.52.0" - headGroupSpec: { ... } - workerGroupSpecs: [ ... ] - gym: { ... } - training: { ... } -``` - -`spec` is the RayCluster `.spec` body passed straight through to the -Kubernetes API, wrapped by the CLI in the `apiVersion: ray.io/v1` + -`kind: RayCluster` + `metadata` envelope. Cross-cutting fields (`image`, -`imagePullSecrets`, `serviceAccountName`) are patched from the top-level -keys, so you don't repeat them. See -`tools/nrl_k8s/src/nrl_k8s/manifest.py`. - -### Resources - -```yaml -resources: - training: - head: {cpu: "8", memory: "32Gi", gpu: null} - worker: {cpu: "96", memory: "768Gi", gpu: 8} - generation: { ... } - gym: { ... } -``` - -The `resources:` block is an escape hatch for when you want the CLI to -derive sensible container `resources:` per role instead of specifying -them inline in `spec`. The shipped examples set container resources -directly inside `spec` (via YAML anchors) for maximum clarity, so the -`resources:` block stays at defaults. - -## Writing a fresh recipe from scratch - -Suppose you want to run a simple colocated SFT-like scenario: one GPU -RayCluster, one training Ray Job, no gym, no generation server. Here's the -minimum pair. - -### Recipe (`examples/sft_llama3_1b.yaml`) - -```yaml -defaults: ../../../examples/configs/sft_llama3.1_1b.yaml - -cluster: - gpus_per_node: 8 - num_nodes: 1 -policy: - train_global_batch_size: 128 - max_total_sequence_length: 4096 -checkpointing: - save_period: 500 -logger: - wandb_enabled: true - wandb: {entity: nvidia, project: nrl-k8s-smoke, name: sft-llama3-1b} -``` - -### Infra (`examples/sft_llama3_1b.infra.yaml`) - -```yaml -namespace: nemo-rl-testing -image: nvcr.io/nvidian/nemo-rl:nightly -imagePullSecrets: [nvidia-ngcuser-pull-secret] -serviceAccount: nemo-rl-endpoint-registry - -launch: - mode: attach - attach: - training: raycluster-sft-llama3-1b - peerWatcher: false - rayUploadPaths: - - nemo_rl - - examples - - infra/examples - entrypoint: | - export GLOO_SOCKET_IFNAME=enp71s0 - export NCCL_SOCKET_IFNAME=enp71s0 - export FI_PROVIDER=efa - python -u examples/run_sft.py --config nrl_k8s_run.yaml - -clusters: - training: - name: raycluster-sft-llama3-1b - spec: - rayVersion: "2.52.0" - headGroupSpec: - rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0"} - template: - spec: - nodeSelector: {gpu-wrangler.nvidia.com/lease: nemo-rl-testing} - tolerations: - - {key: gpu-wrangler.nvidia.com/lease, operator: Equal, value: nemo-rl-testing, effect: NoSchedule} - containers: - - name: ray-head - resources: {limits: {cpu: "8", memory: "32Gi"}} - ports: - - {containerPort: 6379, name: gcs-server} - - {containerPort: 8265, name: dashboard} - - {containerPort: 10001, name: client} - workerGroupSpecs: - - groupName: gpu-workers - replicas: 1 - minReplicas: 1 - maxReplicas: 1 - rayStartParams: {num-gpus: "8"} - template: - spec: - hostNetwork: true - dnsPolicy: ClusterFirstWithHostNet - nodeSelector: {gpu-wrangler.nvidia.com/lease: nemo-rl-testing} - tolerations: - - {key: gpu-wrangler.nvidia.com/lease, operator: Equal, value: nemo-rl-testing, effect: NoSchedule} - containers: - - name: ray-worker - resources: - limits: {cpu: "96", memory: "768Gi", nvidia.com/gpu: "8", vpc.amazonaws.com/efa: "32"} -``` - -Launch: - -```bash -nrl-k8s run examples/sft_llama3_1b.yaml \ - --infra examples/sft_llama3_1b.infra.yaml --follow -``` - -Note what we did *not* declare: no `generation:`, no `gym:`, no daemon, -no `launch.env`. The `run` loop skips undeclared roles (see -`orchestrate.py:218`) so a training-only recipe flows straight through. - -## Adapting an existing recipe to a new cluster - -Assume you want to take `qwen3_4b_if_full_disagg.yaml` + `.infra.yaml` and run them on -a different cluster. Typical changes, in order of likelihood: - -1. **`namespace`** — the one you have permission in. -2. **`image`** and **`imagePullSecrets`** — point at the registry - reachable from the new cluster. The image must ship the NCCL/EFA plugin - your network needs. -3. **`serviceAccount`** — the SA that can read/write - `nemo-rl-endpoints-*` ConfigMaps in the new namespace. -4. **`placement.nodeSelector`** and **`tolerations`** — match your node - labels. In the shipped example these are inlined inside each - `clusters..spec` via anchors, so update both places. -5. **Worker resources** — change - `clusters..spec.workerGroupSpecs[].template.spec.containers[].resources` - to match your instance type. On non-EFA clusters, drop - `vpc.amazonaws.com/efa` and set `hostNetwork: false`. -6. **Environment variables** in the `entrypoint` blocks — - `GLOO_SOCKET_IFNAME`, `NCCL_SOCKET_IFNAME`, `FI_PROVIDER` are specific - to AWS p5.48xlarge. On GCP A3 they're different; on baremetal, drop - them entirely if the image already sets sensible defaults. -7. **`launch.attach.*` names** and **`clusters..name`** — bump these - if you're running the same recipe side-by-side with an existing run - (e.g. append `-v2`). The `submissionId` of each daemon should change too - so Ray accepts the new submission. - -Things that should *not* change when porting: - -- The recipe file. The recipe is cluster-agnostic by design; if you're - editing `policy:`, `grpo:`, etc. to port it, something's off. -- The training `entrypoint`'s python command. Env vars and data paths may - differ; the `python -u examples/.../entry.py --config nrl_k8s_run.yaml - ...` line stays. -- `launch.rayUploadPaths` — governed by what the entrypoint imports, not - by the target cluster. - -## Endpoint registry ConfigMap - -Disaggregated and single-cluster-colocated runs both need the gym and -training halves to find each other. They rendezvous via a Kubernetes -ConfigMap named `nemo-rl-endpoints-` in the namespace. Keys: - -- `gym_head_server` — published by the gym standalone server once its HTTP - listener is up. -- `vllm_base_urls` — published by the generation side (the standalone - gen server in disagg mode; colocated vLLM in single-cluster mode) so - training and gym know where to send generation requests. - -`` comes from the `--job-id` flag on the gym entrypoint (see -`qwen3_4b_if_full_disagg.infra.yaml:221`). The CLI does not have a separate config key -for it — that would just duplicate what's in the entrypoint. - -### How `disagg_job_id` is inferred - -`--replace` wipes the ConfigMap so a new run rendezvouses on fresh keys. -The CLI finds the ConfigMap's name by parsing `--job-id ` from the -**gym daemon entrypoint** string -(`tools/nrl_k8s/src/nrl_k8s/orchestrate.py:228`). If your recipe has no -gym cluster or the gym daemon entrypoint doesn't include `--job-id`, the -reset is skipped silently. Keep the flag on one line and quote-free for -the regex (`--job-id my-id` or `--job-id=my-id`). - -The same `job_id` is fed into the training entrypoint via -`+env.disagg_job_id=` (see `qwen3_4b_if_full_disagg.infra.yaml:116`) so training's -`K8sEndpointRegistry` publishes and reads from the same ConfigMap. - -### What `--replace` cleans up - -As covered in the README: - -1. `nemo-rl-endpoints-` ConfigMap deleted. -2. Running Ray Jobs on any touched cluster stopped (daemons and training). -3. Daemon `submissionId` suffixed with a unix timestamp for the - resubmission — Ray can't reuse IDs even after a terminal state. - -It does *not* delete RayClusters or PVCs; use `nrl-k8s cluster down` for -that. diff --git a/tools/nrl_k8s/docs/roadmap.md b/tools/nrl_k8s/docs/roadmap.md deleted file mode 100644 index 631259cc0d5..00000000000 --- a/tools/nrl_k8s/docs/roadmap.md +++ /dev/null @@ -1,111 +0,0 @@ -# `nrl-k8s` roadmap - -Features that were scaffolded and then removed from the CLI surface pending -real implementations. Re-add the commands as they land so `--help` only -advertises things that work. - -## `doctor` — cluster-baseline health check - -Currently removed. Intended behavior: - -- confirm KubeRay operator ≥ v1.5 is installed and the `RayCluster` CRD is - reachable -- if `infra.scheduler.kind=kai`, confirm the KAI scheduler deployment is - healthy and the `Queue` CRD exists -- if `infra.scheduler.kind=kueue`, confirm the `ClusterQueue` CRD exists -- confirm the configured `serviceAccount` exists in `infra.namespace` -- confirm at least one node advertises `nvidia.com/gpu` in allocatable -- on AWS: confirm the EFA device plugin exposes `vpc.amazonaws.com/efa` - -Exits non-zero on any failed check with a remediation hint per failure. -Good fit for CI preflight and for the `--hint` path of `_explain_and_exit`. - -## `dashboard ` — open the Ray dashboard locally - -Port-forward a RayCluster's head service to a local port and open the URL -(via `webbrowser.open`) in the default browser. Uses the existing -`submit.dashboard_url` context manager. Removed because the happy path is -already covered by `nrl-k8s logs --role training --source daemon` and -`nrl-k8s job list --role …`, but worth re-adding for the "eyeball metrics" -use case. - -## `dev up` / `dev down` — in-cluster dev pod - -Long-running `nrl-dev-` Pod per researcher (one-time create). Image -matches the RayCluster container image so `ray.job_submission.JobSubmission -Client` upload+client Ray versions are identical. Mounts the shared PVC for -code edits and the HF cache PVC. ServiceAccount limited to the minimum -needed to `create` RayClusters + get/list/watch pods + configmaps in -`infra.namespace`. - -Workflow from a laptop becomes: - -```bash -nrl-k8s dev up # one-time -kubectl exec -it pod/nrl-dev-$USER -- bash -# inside: -nrl-k8s run tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml -``` - -Main benefits: no port-forward (the pod is in-cluster so it uses CNI DNS -to reach the Ray dashboard), no laptop-speed upload of the 100 MiB working -dir, and nothing breaks when SSO tokens expire in the middle of a run. - -Removed because the feature requires a shared PVC provisioned per -researcher + RBAC templates; that's a separate platform task. - -## `job list` auto-lookup for training submissions - -Today `job list --role training` returns zero rows because training jobs -have auto-generated submission IDs and there's no stable identifier on -the cluster to correlate a run. A future enhancement tags each submission -with a `wandbRunId` annotation (via `runtime_env.metadata`) so `job list` -can render one line per run with the wandb URL inline. - -## Multi-context support - -`load_kubeconfig` is cached per process; to support running against two -clusters from one shell session (e.g. staging + prod), surface -`--context ` on the root group and thread it down into every -`custom_objects_api()` / `CoreV1Api()` call site. - -## RayJob CRD submission path - -`infra.launch.mode=rayjob` is declared in the schema but `orchestrate.py` -always follows the SDK path. Adding the CRD path means rendering a -`RayJob` manifest with `clusterSelector: {ray.io/cluster: }` and -`submissionMode: HTTPMode`, then applying it via the same manifest builder -used for `RayCluster`. Useful for GitOps pipelines where durable, -k8s-native job state is required. Out of scope for v1.0 unless a caller -shows up for it. - -## Pluggable backends, schedulers, orchestration - -The `backends/`, `schedulers/`, and `orchestration/` subpackages were -sketched early and deleted after a few iterations — none had callers and -the orchestration logic consolidated into a single `orchestrate.py`. Keep -this list so the scaffolding doesn't come back reflexively: - -- **Backends** (`backends/base.py:LaunchBackend`, `kuberay.py`, - `jobset.py`) — abstract layer for non-Ray workloads (JobSet-based pure - PyTorch). Current code assumes KubeRay + `JobSubmissionClient`. If a - JobSet path is needed, add one backend module that implements the same - shape (`apply_cluster`, `submit_job`, `wait_ready`) and flip based on an - `infra.launch.backend: kuberay|jobset` field. -- **Schedulers** (`schedulers/kai.py`, `kueue.py`, `default.py`) — per- - scheduler manifest mutators. `infra.scheduler.kind=kai|kueue` is parsed - into `SchedulerSpec` today but nothing patches the resulting manifest - (no `kai.scheduler/queue` label, no `kueue.x-k8s.io/queue-name`, no - `spec.suspend`). When re-added, wire into `manifest.build_raycluster - _manifest` as a post-patch step keyed on `infra.scheduler.kind`. -- **Orchestration modes** (`orchestration/single.py`, `disagg.py`) — - `LaunchMode` declares `single`, `rayjob`, `attach`, `bringup` but the - code branches only on `.clusters.` presence. If we grow a CRD- - based or bring-up-only path, split the driver in `orchestrate.py` by - mode rather than resurrecting the subpackage. - -The empty `templates/` dir was also removed — the CLI renders RayCluster -objects by patching a Python dict (see `manifest.build_raycluster_ -manifest`). Jinja stays off the table unless the generated YAML stops -fitting into that pattern. From 856e25b6345df64d6b3eaffb8981b0c6bfc8fb28 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 22:39:24 -0700 Subject: [PATCH 52/84] =?UTF-8?q?chore:=20sanitize=20configs=20=E2=80=94?= =?UTF-8?q?=20use=20nightly=20image,=20main=20branch,=20generic=20node=20n?= =?UTF-8?q?ames?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Terry Kong --- infra/examples/disagg-jobset.yaml | 16 ++++++++-------- infra/examples/disagg-rayclusters.yaml | 6 +++--- infra/examples/monolithic-jobset.yaml | 2 +- infra/examples/rayjob-monolithic.yaml | 4 ++-- .../qwen3_30b_math_8n_4gpu.gb300.infra.yaml | 6 +++--- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/infra/examples/disagg-jobset.yaml b/infra/examples/disagg-jobset.yaml index 372934f9392..08c131fb9eb 100644 --- a/infra/examples/disagg-jobset.yaml +++ b/infra/examples/disagg-jobset.yaml @@ -90,7 +90,7 @@ spec: - name: nvcr-secret containers: - name: ray-head - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + image: nvcr.io/nvidian/nemo-rl:nightly command: ["/bin/bash", "-c"] args: - | @@ -145,7 +145,7 @@ spec: - name: nvcr-secret initContainers: - name: wait-for-rl-head - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + image: nvcr.io/nvidian/nemo-rl:nightly command: ["/bin/bash", "-c"] args: - | @@ -159,7 +159,7 @@ spec: requests: { cpu: "200m", memory: "256Mi" } containers: - name: ray-worker - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + image: nvcr.io/nvidian/nemo-rl:nightly command: ["/bin/bash", "-c"] args: - | @@ -205,7 +205,7 @@ spec: - name: nvcr-secret containers: - name: ray-head - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + image: nvcr.io/nvidian/nemo-rl:nightly command: ["/bin/bash", "-c"] args: - | @@ -265,7 +265,7 @@ spec: - name: nvcr-secret initContainers: - name: wait-for-gym-head - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + image: nvcr.io/nvidian/nemo-rl:nightly command: ["/bin/bash", "-c"] args: - | @@ -279,7 +279,7 @@ spec: requests: { cpu: "200m", memory: "256Mi" } containers: - name: ray-worker - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + image: nvcr.io/nvidian/nemo-rl:nightly command: ["/bin/bash", "-c"] args: - | @@ -322,7 +322,7 @@ spec: - name: nvcr-secret initContainers: - name: wait-for-rl-head - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + image: nvcr.io/nvidian/nemo-rl:nightly command: ["/bin/bash", "-c"] args: - | @@ -336,7 +336,7 @@ spec: requests: { cpu: "200m", memory: "256Mi" } containers: - name: driver - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + image: nvcr.io/nvidian/nemo-rl:nightly command: ["/bin/bash", "-c"] args: - | diff --git a/infra/examples/disagg-rayclusters.yaml b/infra/examples/disagg-rayclusters.yaml index 74906620bfd..7633c9d2d16 100644 --- a/infra/examples/disagg-rayclusters.yaml +++ b/infra/examples/disagg-rayclusters.yaml @@ -64,7 +64,7 @@ spec: containers: # --- Ray head --- - name: ray-head - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + image: nvcr.io/nvidian/nemo-rl:nightly env: - name: RAY_CLUSTER_NAME value: raycluster-rl # used by endpoint registry for ownerReference @@ -173,7 +173,7 @@ spec: - name: nvcr-secret containers: - name: ray-worker - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + image: nvcr.io/nvidian/nemo-rl:nightly env: - name: NRL_FORCE_REBUILD_VENVS value: "true" @@ -211,7 +211,7 @@ spec: containers: # --- Gym Ray head (runs standalone_gym_server) --- - name: ray-head - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + image: nvcr.io/nvidian/nemo-rl:nightly resources: limits: { cpu: "4", memory: "16Gi" } requests: { cpu: "1", memory: "4Gi" } diff --git a/infra/examples/monolithic-jobset.yaml b/infra/examples/monolithic-jobset.yaml index d8c8e15875e..06fe576b56a 100644 --- a/infra/examples/monolithic-jobset.yaml +++ b/infra/examples/monolithic-jobset.yaml @@ -46,7 +46,7 @@ # Allocates RoCE (RDMA) NICs per worker pod for inter-node communication. # This is a separate DRA driver (dra.networking.k8s.aws) — you create it yourself. # Adjust the count based on your node type: -# - GB300 (p6e-gb300r.36xlarge): count: 8 (8 RoCE NICs per node) +# - GB300 ( NVL72 GB300 node): count: 8 (8 RoCE NICs per node) # # Only GPU worker pods need these claims (referenced via resources.claims in the # container spec and resourceClaims in the pod spec). diff --git a/infra/examples/rayjob-monolithic.yaml b/infra/examples/rayjob-monolithic.yaml index 794c163fc25..e61c279c0d8 100644 --- a/infra/examples/rayjob-monolithic.yaml +++ b/infra/examples/rayjob-monolithic.yaml @@ -33,7 +33,7 @@ spec: - name: nvcr-secret containers: - name: ray-head - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + image: nvcr.io/nvidian/nemo-rl:nightly env: - name: NRL_FORCE_REBUILD_VENVS value: "true" @@ -73,7 +73,7 @@ spec: - name: nvcr-secret containers: - name: ray-worker - image: nvcr.io/nvidian/nemo-rl:e5a729c-47084432 + image: nvcr.io/nvidian/nemo-rl:nightly env: - name: NRL_FORCE_REBUILD_VENVS value: "true" diff --git a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml index fac69041fe0..3a3490405ed 100644 --- a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml @@ -1,4 +1,4 @@ -# Prod-mode infra for qwen3_30b_math_8n_4gpu.yaml on GB300 (p6e-gb300r.36xlarge). +# Prod-mode infra for qwen3_30b_math_8n_4gpu.yaml on a GB300 NVL72 cluster. # # Topology: 8 workers × 4 GPUs = 32 GPUs in a single RayCluster, scheduled by # KAI as one gang. NVLink/MNNVL spans all 32 GPUs via a ComputeDomain channel, @@ -17,7 +17,7 @@ # Prereqs (onboarding §3–§5 already applied in `default`): # - rl-workspace PVC bound (shared FSx Lustre, 2.4 TiB, RWX). # - /mnt/rl-workspace/${user:}/nemo-rl contains a git checkout of -# https://github.com/NVIDIA-NeMo/RL on branch hemil/k8s-infra-cp. +# https://github.com/NVIDIA-NeMo/RL on branch main. # - nvcr-secret, wandb-api-key, nemo-rl-endpoint-registry all present. # - KAI topology `gb300-topology` is registered and advertises # nvidia.com/gpu.clique as a placement key. @@ -60,7 +60,7 @@ _shared: limits: {cpu: "60", memory: "240Gi"} requests: {cpu: "48", memory: "200Gi"} gpuWorkerResources: &gpu_worker_resources - # p6e-gb300r.36xlarge allocatable: ~139.6 CPU / ~924 GiB / 4 GPUs. + # NVL72 GB300 node allocatable: ~140 CPU / ~924 GiB / 4 GPUs. # Claim 120/132 CPU + 850/900 GiB to leave headroom for dgxc/device-plugin. limits: cpu: "132" From 1c5fc931b115b1e5e2c94ec43de94be81786b306 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 22:42:23 -0700 Subject: [PATCH 53/84] =?UTF-8?q?docs:=20update=20launch-nemo-rl=20skill?= =?UTF-8?q?=20=E2=80=94=20fix=20default=20mode,=20DRA=20auto-manage,=20rem?= =?UTF-8?q?ove=20dead=20refs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Terry Kong --- skills/launch-nemo-rl/SKILL.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/skills/launch-nemo-rl/SKILL.md b/skills/launch-nemo-rl/SKILL.md index 6ccd89a1146..06b5b3b75c3 100644 --- a/skills/launch-nemo-rl/SKILL.md +++ b/skills/launch-nemo-rl/SKILL.md @@ -1,6 +1,6 @@ --- name: launch-nemo-rl -description: Playbook for launching, monitoring, stopping, and debugging NeMo-RL recipes on a Kubernetes cluster via the nrl-k8s CLI. Use when asked to run/launch/submit/test a recipe on k8s, bring up or tear down a RayCluster, choose between long-lived (default `nrl-k8s run`) and ephemeral (`nrl-k8s run --rayjob`, auto-teardown) modes, iterate on an existing run, debug a hung or failed training job, or structure a new recipe+infra pair for a new hardware profile. +description: Playbook for launching, monitoring, stopping, and debugging NeMo-RL recipes on a Kubernetes cluster via the nrl-k8s CLI. Use when asked to run/launch/submit/test a recipe on k8s, bring up or tear down a RayCluster, choose between ephemeral (default `nrl-k8s run`, auto-teardown) and long-lived (`nrl-k8s run --raycluster`) modes, iterate on an existing run, debug a hung or failed training job, or structure a new recipe+infra pair for a new hardware profile. when_to_use: "run this recipe on k8s", "launch on the cluster", "submit a training job", "tear down the cluster", "resubmit as rayjob", "why is the run stuck", "how do I get logs for job X", "bring the cluster back up". allowed-tools: Bash Read Grep Glob Edit Write --- @@ -15,10 +15,10 @@ There is a single top-level submission command: **`nrl-k8s run`**. It has two li | Mode | Invocation | When to use | Cluster after? | | :----------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------- | -| Long-lived (default) | `nrl-k8s run` | Dev loop. Reuses a matching live cluster, applies if absent, warns + reuses on drift (pass `--recreate` to replace). Then submits daemons and training. First-choice for iteration. | Yes | -| Ephemeral | `nrl-k8s run --rayjob` | One-shot. KubeRay applies a RayJob, runs, tears the cluster down. Best for paper / eval runs where you don't plan to resubmit. | No (auto) | +| Ephemeral (default) | `nrl-k8s run` | One-shot. KubeRay applies a RayJob, runs, tears the cluster down. Best for most runs. | No (auto) | +| Long-lived | `nrl-k8s run --raycluster` | Dev loop. Reuses a matching live cluster, applies if absent, warns + reuses on drift (pass `--recreate` to replace). Then submits daemons and training. First-choice for iteration. | Yes | -Ask: *Do I need this cluster after the run?* If no, use `--rayjob`. If yes, plain `run`. +Ask: *Do I need this cluster after the run?* If yes, use `--raycluster`. Otherwise use the default (ephemeral). The rest of the CLI is observability / stage-by-stage control: @@ -103,21 +103,18 @@ Every infra YAML encodes a hardware/scheduler profile. The concrete examples in - **Per-node GPUs** (e.g. 4 vs 8) — must match `cluster.gpus_per_node` in the recipe, otherwise workers stay `Pending`. - **Node selectors** — head pods usually land on a CPU-only node pool; GPU workers match on `nvidia.com/gpu.product` or a node-group label. - **Scheduler** — KAI (`schedulerName: kai-scheduler` + `kai.scheduler/queue` label) with topology annotations (`kai.scheduler/topology`, `kai.scheduler/topology-required-placement`) gang-schedules workers into one clique. Without it, pods may land on different racks and NVLink/RoCE won't span them. -- **DRA claims** — ComputeDomain + RoCE are attached via `resourceClaims` referencing `ResourceClaimTemplate`s that must pre-exist in the namespace. Missing templates = `RayCluster` Pending forever. +- **DRA claims** — ComputeDomain + RoCE are attached via `resourceClaims` referencing `ResourceClaimTemplate`s. The CLI auto-creates/deletes these when the worker pod spec contains DRA claim references — no manual setup needed. - **Secrets** — always via `secretKeyRef` (`wandb-api-key`, image pull secret). Never embed. - **Shared filesystem mounts** — typically a Lustre PVC mounted twice: once at the code path (e.g. `/opt/nemo-rl` with a user-scoped `subPath`) and once at a workspace root (e.g. `/mnt/rl-workspace`) for datasets, HF cache, and checkpoints. Before applying an infra, verify prereqs exist in the target namespace: ```bash -kubectl get resourceclaimtemplate kubectl get pvc kubectl get secret kubectl get sa ``` -See `tools/nrl_k8s/docs/onboarding.md` for ComputeDomain CR + RoCE ResourceClaimTemplate shapes and the full onboarding checklist for a new namespace. - ## 7. End-to-end workflows ### 7a. Fresh one-shot run (rayjob) @@ -228,7 +225,6 @@ Before reporting a launch as successful, verify: ## 13. Where things live in the repo - CLI code: `tools/nrl_k8s/src/nrl_k8s/` (`cli.py`, `orchestrate.py`, `manifest.py`, `rayjob.py`, `k8s.py`, `submitters/`, `schema.py`). -- Tests: `tools/nrl_k8s/tests/unit/` — run with `uv run --active pytest tests/unit -x` from `tools/nrl_k8s/`. +- Tests: `tools/nrl_k8s/tests/unit/` — run with `uv run --extra test pytest -x -q` from `tools/nrl_k8s/`. - Recipe + infra examples: `tools/nrl_k8s/examples/`. -- Onboarding + recipes docs: `tools/nrl_k8s/docs/`. - Base recipes this tool wraps: `examples/configs/recipes/llm/…` and `examples/nemo_gym/…`. From 9afa4ecca005a002f8d75d3902f3fedc7cd38ba9 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 23:05:34 -0700 Subject: [PATCH 54/84] docs: remove stale TODO sections from infra README Signed-off-by: Terry Kong --- infra/README.md | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/infra/README.md b/infra/README.md index 3f339ac2b8f..a56d3972977 100644 --- a/infra/README.md +++ b/infra/README.md @@ -235,23 +235,3 @@ KAI distributes GPU resources using hierarchical fair-share with two phases: - `kai-queue.yaml` — 2-GPU kind cluster (high-prio + low-prio) - `kai-queue-prod.yaml` — 288-GPU NVL72 production cluster (priority + community departments) - -## TODO: NVL72 topology-aware scheduling - -KAI v0.14.0 added Ray topology-aware subgroup scheduling ([PR #1125](https://github.com/kai-scheduler/KAI-Scheduler/pull/1125)). Need to test on an actual NVL72 cluster: - -- **Confirm `--segment=N` equivalent works**: KAI's `subGroups` with per-subgroup `topologyConstraint.requiredTopologyLevel: "rack"` should be the equivalent of Slurm's `--segment=N`. Each subgroup of N nodes is constrained to one rack. Unclear if this works correctly for cross-rack scheduling (e.g., `--segment=16` with 32 total nodes = 2 racks). -- **Auto-segmentation not yet implemented**: The design doc at [`docs/developer/designs/segmented-subgroups/`](https://github.com/kai-scheduler/KAI-Scheduler/blob/main/docs/developer/designs/segmented-subgroups/README.md) proposes `kai.scheduler/segment-size` annotation for automatic subgroup creation, but it depends on "Replica-Type SubGrouping" which isn't shipped yet. See [Issue #1189](https://github.com/kai-scheduler/KAI-Scheduler/issues/1189) and [PR #1127](https://github.com/kai-scheduler/KAI-Scheduler/pull/1127) (minSubGroup field, still open). - -## TODO: Log persistence - -Currently, logs are lost when RayJob pods are cleaned up (`ttlSecondsAfterFinished`). Two levels of log persistence are needed: - -1. **Container stdout/stderr** (`kubectl logs`): Captured by containerd at `/var/log/pods/` on the node, but not queryable after pod deletion. -2. **Ray file logs** (`/tmp/ray/session_*/logs/`): Worker/driver logs, system logs — not sent to stdout at all. - -**Planned approach**: Deploy a Loki stack (Loki + Promtail + Grafana) via helmfile for both `kind` and `prod` environments: -- Promtail DaemonSet captures container stdout/stderr from each node, auto-labels with K8s metadata (namespace, pod, job name) -- Fluent Bit sidecar in each RayJob pod tails `/tmp/ray` logs and ships to Loki -- Grafana UI for querying logs by job name, time range, and content (LogQL) -- kind: Loki stores on local PVC; prod: Loki stores on S3/GCS From 4b9632942753491c4a4779a4895bf21c21a14aaf Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 23:09:41 -0700 Subject: [PATCH 55/84] docs: fix diagram alignment in infra README Signed-off-by: Terry Kong --- infra/README.md | 45 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/infra/README.md b/infra/README.md index a56d3972977..1bf88429769 100644 --- a/infra/README.md +++ b/infra/README.md @@ -90,15 +90,15 @@ This installs KAI scheduler, KubeRay, and JobSet. The cluster is expected to alr All components run on a single RayCluster — vLLM generation, Megatron training, and Gym environment servers are colocated as Ray actors on the same cluster. ``` -┌─────────────────────────────────────────────┐ -│ RayCluster / RayJob │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ vLLM │ │ Megatron │ │ Gym │ │ -│ │ (GPU) │ │ (GPU) │ │ (CPU) │ │ -│ └──────────┘ └──────────┘ └──────────┘ │ -│ All on same Ray cluster │ -└─────────────────────────────────────────────┘ +┌───────────────────────────────────────────────┐ +│ RayCluster / RayJob │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ vLLM │ │ Megatron │ │ Gym │ │ +│ │ (GPU) │ │ (GPU) │ │ (CPU) │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ All on same Ray cluster │ +└───────────────────────────────────────────────┘ ``` Example: `rayjob-monolithic.yaml` — a single RayJob with head + GPU workers. KubeRay manages the lifecycle. @@ -108,22 +108,19 @@ Example: `rayjob-monolithic.yaml` — a single RayJob with head + GPU workers. K The RL cluster (vLLM + Megatron) and Gym cluster (environment servers) run independently and communicate over HTTP. A K8s ConfigMap acts as an endpoint registry for dynamic URL exchange — RL publishes vLLM URLs, Gym publishes its head server address. ``` -┌──────────────────────────┐ ┌──────────────────────────┐ -│ RL Ray Cluster │ │ Gym Ray Cluster │ -│ │ │ │ -│ ┌──────┐ ┌──────────┐ │ │ ┌──────────┐ │ -│ │ vLLM │ │ Megatron │ │ │ │ Gym │ │ -│ │ (GPU)│ │ (GPU) │ │ │ │ servers │ │ -│ └──┬───┘ └──────────┘ │ │ └────┬─────┘ │ -│ │ │ │ │ │ -└─────┼────────────────────┘ └───────┼───────────────────┘ +┌───────────────────────────┐ ┌───────────────────────────┐ +│ RL Ray Cluster │ │ Gym Ray Cluster │ +│ │ │ │ +│ ┌──────┐ ┌──────────┐ │ │ ┌──────────┐ │ +│ │ vLLM │ │ Megatron │ │ │ │ Gym │ │ +│ │ (GPU)│ │ (GPU) │ │ │ │ servers │ │ +│ └──┬───┘ └──────────┘ │ │ └────┬─────┘ │ +│ │ │ │ │ │ +└─────┼─────────────────────┘ └───────┼───────────────────┘ │ │ - │ ┌──────────────────┐ │ - └────►│ ConfigMap │◄────────┘ - │ (endpoint │ - │ registry) │ - └──────────────────┘ - vLLM URLs ←→ Gym address + │ ┌──────────────────┐ │ + └───►│ ConfigMap │◄─────────┘ + └──────────────────┘ ``` There are two ways to deploy the disagg architecture: From 64baa74b72444430627c71fce9deea6a15abacb4 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Fri, 24 Apr 2026 00:19:09 -0700 Subject: [PATCH 56/84] fix: scale qwen3-30b recipe to 4 nodes, fix stale defaults path Signed-off-by: Terry Kong --- ...> qwen3_30b_math_4n_4gpu.gb300.infra.yaml} | 28 +++++++++---------- ..._4gpu.yaml => qwen3_30b_math_4n_4gpu.yaml} | 14 +++++----- 2 files changed, 21 insertions(+), 21 deletions(-) rename tools/nrl_k8s/examples/{qwen3_30b_math_8n_4gpu.gb300.infra.yaml => qwen3_30b_math_4n_4gpu.gb300.infra.yaml} (91%) rename tools/nrl_k8s/examples/{qwen3_30b_math_8n_4gpu.yaml => qwen3_30b_math_4n_4gpu.yaml} (57%) diff --git a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml b/tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml similarity index 91% rename from tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml rename to tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml index 3a3490405ed..ccacc65e6fe 100644 --- a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml +++ b/tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml @@ -1,8 +1,8 @@ -# Prod-mode infra for qwen3_30b_math_8n_4gpu.yaml on a GB300 NVL72 cluster. +# Prod-mode infra for qwen3_30b_math_4n_4gpu.yaml on a GB300 NVL72 cluster. # -# Topology: 8 workers × 4 GPUs = 32 GPUs in a single RayCluster, scheduled by -# KAI as one gang. NVLink/MNNVL spans all 32 GPUs via a ComputeDomain channel, -# and NCCL rides 8× RoCE NICs per node — both attached through DRA. The CLI +# Topology: 4 workers × 4 GPUs = 16 GPUs in a single RayCluster, scheduled by +# KAI as one gang. NVLink/MNNVL spans all 16 GPUs via a ComputeDomain channel, +# and NCCL rides 4× RoCE NICs per node — both attached through DRA. The CLI # auto-creates and auto-deletes the ComputeDomain + RoCE ResourceClaimTemplate # based on the resourceClaims in the worker pod spec. # @@ -25,13 +25,13 @@ # Usage: # # Long-lived cluster — idempotent; reuses the live cluster if it # # already matches, applies if absent. Submits training per invocation. -# nrl-k8s run tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml \ +# nrl-k8s run tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml \ # --run-id qwen3-30b-math-$(date +%Y%m%d-%H%M%S) # # # One-shot with auto-teardown (ephemeral RayCluster, no cleanup needed): -# nrl-k8s rayjob tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.gb300.infra.yaml +# nrl-k8s rayjob tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml _shared: headNodeSelector: &head_node_selector @@ -135,7 +135,7 @@ launch: mkdir -p "\${LOG_DIR}" python -u examples/run_grpo.py \ - --config tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml \ + --config tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml \ logger.wandb_enabled=true \ logger.wandb.project=nemorl-single-k8s \ logger.wandb.name=qwen3-30b-math-gb300 \ @@ -170,9 +170,9 @@ clusters: volumes: *head_volumes workerGroupSpecs: - groupName: gpu-workers - replicas: 8 - minReplicas: 8 - maxReplicas: 8 + replicas: 4 + minReplicas: 4 + maxReplicas: 4 numOfHosts: 1 rayStartParams: num-gpus: "4" @@ -180,8 +180,8 @@ clusters: template: metadata: annotations: - # KAI co-schedules the 8 workers in a single gb300-topology - # domain so the ComputeDomain NVLink channel spans all 32 GPUs. + # KAI co-schedules the 4 workers in a single gb300-topology + # domain so the ComputeDomain NVLink channel spans all 16 GPUs. kai.scheduler/topology: gb300-topology kai.scheduler/topology-required-placement: nvidia.com/gpu.clique spec: diff --git a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml b/tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml similarity index 57% rename from tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml rename to tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml index 86d4d3fc26d..c01b6f59d14 100644 --- a/tools/nrl_k8s/examples/qwen3_30b_math_8n_4gpu.yaml +++ b/tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml @@ -1,16 +1,16 @@ -# Qwen3-30B-A3B GRPO on math — **single RayCluster, 8 nodes × 4 GPUs (GB300)**. +# Qwen3-30B-A3B GRPO on math — **single RayCluster, 4 nodes × 4 GPUs (GB300)**. # # Extends the performance async-1off recipe (training + generation inside one -# Ray cluster, non-colocated split: 4 nodes training, 4 nodes generation). -# The 32 workers are a single pod-group on GB300 so NVLink spans all 32 GPUs +# Ray cluster, non-colocated split: 2 nodes training, 2 nodes generation). +# The 16 workers are a single pod-group on GB300 so NVLink spans all 16 GPUs # via a ComputeDomain channel claim (see the paired infra file). # -# Pair with qwen3_30b_math_8n_4gpu.gb300.infra.yaml. -defaults: ../../../examples/configs/recipes/llm/performance/grpo-qwen3-30ba3b-8n4g-async-1off.yaml +# Pair with qwen3_30b_math_4n_4gpu.gb300.infra.yaml. +defaults: ../../../examples/configs/recipes/llm/performance/grpo-qwen3-30ba3b-4n4g-async-1off.yaml cluster: gpus_per_node: 4 - num_nodes: 8 + num_nodes: 4 grpo: max_num_steps: 500 val_period: 50 @@ -20,6 +20,6 @@ logger: wandb_enabled: true wandb: project: nemorl-single-k8s - name: qwen3-30b-math-8n-4gpu-gb300 + name: qwen3-30b-math-4n-4gpu-gb300 tensorboard_enabled: true monitor_gpus: true From c7817d4596fd0b2724d79a5ac7989811a15c4019 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Fri, 24 Apr 2026 14:41:26 -0700 Subject: [PATCH 57/84] =?UTF-8?q?feat(nrl-k8s):=20dev=20pod=20improvements?= =?UTF-8?q?=20=E2=80=94=20USER=20env,=20RBAC=20check,=20kubectl=20install,?= =?UTF-8?q?=20image=20mismatch=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Terry Kong --- skills/launch-nemo-rl/SKILL.md | 30 +++++++++++++++++++++- tools/nrl_k8s/src/nrl_k8s/cli.py | 44 ++++++++++++++++++++++++++++++++ tools/nrl_k8s/src/nrl_k8s/dev.py | 10 ++++++++ tools/nrl_k8s/src/nrl_k8s/k8s.py | 16 ++++++++++++ 4 files changed, 99 insertions(+), 1 deletion(-) diff --git a/skills/launch-nemo-rl/SKILL.md b/skills/launch-nemo-rl/SKILL.md index 06b5b3b75c3..3d5caa4c6b9 100644 --- a/skills/launch-nemo-rl/SKILL.md +++ b/skills/launch-nemo-rl/SKILL.md @@ -222,7 +222,35 @@ Before reporting a launch as successful, verify: 4. At least one `Processed prompts: 100%` line appears (confirms generation is wired). 5. For `--rayjob` mode only: after `jobDeploymentStatus=Complete`, confirm `kubectl get raycluster | grep ` is empty (teardown worked). -## 13. Where things live in the repo +## 13. Dev pod + +`nrl-k8s dev` manages a lightweight CPU pod on the cluster for code syncing, debugging, and running `kubectl`/`nrl-k8s` from within the cluster. + +```bash +# One-time: set up secrets (HF token, wandb, SSH key, rclone) +nrl-k8s dev setup-secrets --ssh-key ~/.ssh/id_rsa --add-rclone + +# Create pod and exec in (idempotent — reuses existing pod) +nrl-k8s dev connect + +# Switch image (must stop first — image change is warned but not auto-applied) +nrl-k8s dev stop +nrl-k8s dev connect --image nvcr.io/nvidian/nemo-rl:v0.7.0 + +# Tear down +nrl-k8s dev stop +``` + +The dev pod: +- Runs on a CPU-only node (anti-affinity to GPU nodes) +- Mounts the shared `rl-workspace` PVC at `/mnt/rl-workspace` +- Sets `USER` env var to the `nrl-k8s` username (so `$USER` and `getpass.getuser()` work correctly despite running as root) +- Installs `kubectl`, `rclone` (if configured) on first boot +- Injects SSH keys and tokens via `envFrom` on a per-user K8s Secret + +The pod's `default` service account needs an `edit` RoleBinding in the namespace for `kubectl` to work inside. `dev connect` checks this and prints the required YAML if missing. + +## 14. Where things live in the repo - CLI code: `tools/nrl_k8s/src/nrl_k8s/` (`cli.py`, `orchestrate.py`, `manifest.py`, `rayjob.py`, `k8s.py`, `submitters/`, `schema.py`). - Tests: `tools/nrl_k8s/tests/unit/` — run with `uv run --extra test pytest -x -q` from `tools/nrl_k8s/`. diff --git a/tools/nrl_k8s/src/nrl_k8s/cli.py b/tools/nrl_k8s/src/nrl_k8s/cli.py index a8527d2e049..5456671f291 100644 --- a/tools/nrl_k8s/src/nrl_k8s/cli.py +++ b/tools/nrl_k8s/src/nrl_k8s/cli.py @@ -1194,12 +1194,22 @@ def dev_connect(image: str, namespace: str | None) -> None: if namespace is None: namespace = _infer_namespace() + _check_dev_pod_rbac(namespace) + phase = k8s.get_pod_phase(pod_name, namespace) if phase is None: click.echo(f"creating dev pod {pod_name} in {namespace} ...") manifest = build_dev_pod_manifest(user, namespace, image) k8s.create_pod(manifest, namespace) phase = "Pending" + else: + running_image = k8s.get_pod_image(pod_name, namespace) + if running_image and running_image != image: + click.echo( + f"warning: dev pod is using image {running_image}, " + f"not {image} — stop and reconnect to switch", + err=True, + ) if phase != "Running": click.echo(f"waiting for {pod_name} to be Running ...") @@ -1411,6 +1421,40 @@ def _rayjob_status(name: str) -> str | None: _error_on_stale(_find_stale_resources(checks), namespace) +def _check_dev_pod_rbac(namespace: str) -> None: + """Verify the default SA has edit access so kubectl works inside the dev pod.""" + import subprocess + + sa = f"system:serviceaccount:{namespace}:default" + result = subprocess.run( + ["kubectl", "auth", "can-i", "get", "pods", + f"--as={sa}", "-n", namespace], + capture_output=True, text=True, + ) + if result.stdout.strip() == "yes": + return + rolebinding = ( + f"apiVersion: rbac.authorization.k8s.io/v1\n" + f"kind: RoleBinding\n" + f"metadata:\n" + f" name: default-sa-edit\n" + f" namespace: {namespace}\n" + f"subjects:\n" + f" - kind: ServiceAccount\n" + f" name: default\n" + f" namespace: {namespace}\n" + f"roleRef:\n" + f" kind: ClusterRole\n" + f" name: edit\n" + f" apiGroup: rbac.authorization.k8s.io" + ) + _cli_error( + f"the default service account in {namespace} lacks edit permissions — " + f"kubectl won't work inside the dev pod", + hint=f"apply this RoleBinding, then retry:\n\n{rolebinding}", + ) + + def _preflight_or_exit(namespace: str) -> None: """Fail fast when kubectl is missing or RBAC is wrong — before we spawn anything.""" from . import submit diff --git a/tools/nrl_k8s/src/nrl_k8s/dev.py b/tools/nrl_k8s/src/nrl_k8s/dev.py index f9c557c0eb5..5309846ec9b 100644 --- a/tools/nrl_k8s/src/nrl_k8s/dev.py +++ b/tools/nrl_k8s/src/nrl_k8s/dev.py @@ -32,6 +32,11 @@ def build_dev_pod_manifest( "curl -sSf https://rclone.org/install.sh | bash; " "fi; " "fi && " + "if ! command -v kubectl >/dev/null 2>&1; then " + 'ARCH=$(uname -m | sed "s/x86_64/amd64/;s/aarch64/arm64/") && ' + 'curl -sLo /usr/local/bin/kubectl "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/${ARCH}/kubectl" && ' + "chmod +x /usr/local/bin/kubectl; " + "fi && " "sleep infinity" ) @@ -72,6 +77,11 @@ def build_dev_pod_manifest( "image": image, "command": ["sh", "-c", command], "workingDir": user_dir, + # Set USER so getpass.getuser() / $USER returns the real + # owner, not "root". We keep uid=0 so users can apt-install. + # nrl-k8s jobs submitted from the dev pod use this to tag + # ownership — without it every submitter shows up as "root". + "env": [{"name": "USER", "value": username}], "envFrom": [{"secretRef": {"name": secret_name, "optional": True}}], "resources": { "requests": {"cpu": "100m", "memory": "256Mi"}, diff --git a/tools/nrl_k8s/src/nrl_k8s/k8s.py b/tools/nrl_k8s/src/nrl_k8s/k8s.py index d3a1c960bd1..a2fa66f4c98 100644 --- a/tools/nrl_k8s/src/nrl_k8s/k8s.py +++ b/tools/nrl_k8s/src/nrl_k8s/k8s.py @@ -467,6 +467,22 @@ def get_pod_phase(name: str, namespace: str) -> str | None: raise +def get_pod_image(name: str, namespace: str) -> str | None: + """Return the image of the first container in a pod, or None if not found.""" + load_kubeconfig() + core = client.CoreV1Api() + try: + pod = with_retries( + lambda: core.read_namespaced_pod(name=name, namespace=namespace) + ) + containers = pod.spec.containers or [] + return containers[0].image if containers else None + except ApiException as exc: + if exc.status == 404: + return None + raise + + # ============================================================================= # Secrets # ============================================================================= From badf7015ee33224e47cec6101f033e49b4e7bff8 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 26 Apr 2026 11:42:23 -0700 Subject: [PATCH 58/84] refactor: move tools/nrl_k8s to infra/nrl_k8s, update all path references Signed-off-by: Terry Kong --- {tools => infra}/nrl_k8s/README.md | 70 +- .../qwen3_30b_math_4n_4gpu.gb300.infra.yaml | 10 +- .../examples/qwen3_30b_math_4n_4gpu.yaml | 0 {tools => infra}/nrl_k8s/pyproject.toml | 0 .../nrl_k8s/src/nrl_k8s/__init__.py | 0 .../nrl_k8s/src/nrl_k8s/_logging.py | 0 .../nrl_k8s/src/nrl_k8s/_retry.py | 0 {tools => infra}/nrl_k8s/src/nrl_k8s/cli.py | 2 +- .../nrl_k8s/src/nrl_k8s/config.py | 0 .../nrl_k8s/defaults/defaults.example.yaml | 0 {tools => infra}/nrl_k8s/src/nrl_k8s/dev.py | 0 .../nrl_k8s/src/nrl_k8s/inspect.py | 0 {tools => infra}/nrl_k8s/src/nrl_k8s/k8s.py | 0 .../nrl_k8s/src/nrl_k8s/manifest.py | 0 .../nrl_k8s/src/nrl_k8s/orchestrate.py | 0 .../nrl_k8s/src/nrl_k8s/rayjob.py | 0 .../nrl_k8s/src/nrl_k8s/schema.py | 0 .../nrl_k8s/src/nrl_k8s/submit.py | 0 .../src/nrl_k8s/submitters/__init__.py | 0 .../nrl_k8s/src/nrl_k8s/submitters/exec_.py | 0 .../src/nrl_k8s/submitters/portforward.py | 0 .../nrl_k8s/src/nrl_k8s/workdir.py | 0 {tools => infra}/nrl_k8s/tests/__init__.py | 0 .../nrl_k8s/tests/unit/__init__.py | 0 .../nrl_k8s/tests/unit/test_cli.py | 0 .../nrl_k8s/tests/unit/test_config.py | 0 .../nrl_k8s/tests/unit/test_inspect.py | 0 .../nrl_k8s/tests/unit/test_k8s.py | 0 .../nrl_k8s/tests/unit/test_manifest.py | 0 .../nrl_k8s/tests/unit/test_orchestrate.py | 0 .../nrl_k8s/tests/unit/test_rayjob.py | 0 .../nrl_k8s/tests/unit/test_schema.py | 0 .../nrl_k8s/tests/unit/test_submit.py | 0 .../nrl_k8s/tests/unit/test_submitters.py | 0 .../nrl_k8s/tests/unit/test_workdir.py | 0 infra/nrl_k8s/uv.lock | 2310 +++++++++++++++++ skills/launch-nemo-rl/SKILL.md | 18 +- 37 files changed, 2360 insertions(+), 50 deletions(-) rename {tools => infra}/nrl_k8s/README.md (91%) rename {tools => infra}/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml (96%) rename {tools => infra}/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml (100%) rename {tools => infra}/nrl_k8s/pyproject.toml (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/__init__.py (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/_logging.py (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/_retry.py (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/cli.py (99%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/config.py (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/defaults/defaults.example.yaml (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/dev.py (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/inspect.py (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/k8s.py (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/manifest.py (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/orchestrate.py (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/rayjob.py (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/schema.py (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/submit.py (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/submitters/__init__.py (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/submitters/exec_.py (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/submitters/portforward.py (100%) rename {tools => infra}/nrl_k8s/src/nrl_k8s/workdir.py (100%) rename {tools => infra}/nrl_k8s/tests/__init__.py (100%) rename {tools => infra}/nrl_k8s/tests/unit/__init__.py (100%) rename {tools => infra}/nrl_k8s/tests/unit/test_cli.py (100%) rename {tools => infra}/nrl_k8s/tests/unit/test_config.py (100%) rename {tools => infra}/nrl_k8s/tests/unit/test_inspect.py (100%) rename {tools => infra}/nrl_k8s/tests/unit/test_k8s.py (100%) rename {tools => infra}/nrl_k8s/tests/unit/test_manifest.py (100%) rename {tools => infra}/nrl_k8s/tests/unit/test_orchestrate.py (100%) rename {tools => infra}/nrl_k8s/tests/unit/test_rayjob.py (100%) rename {tools => infra}/nrl_k8s/tests/unit/test_schema.py (100%) rename {tools => infra}/nrl_k8s/tests/unit/test_submit.py (100%) rename {tools => infra}/nrl_k8s/tests/unit/test_submitters.py (100%) rename {tools => infra}/nrl_k8s/tests/unit/test_workdir.py (100%) create mode 100644 infra/nrl_k8s/uv.lock diff --git a/tools/nrl_k8s/README.md b/infra/nrl_k8s/README.md similarity index 91% rename from tools/nrl_k8s/README.md rename to infra/nrl_k8s/README.md index d2a7713a97c..351cefc0097 100644 --- a/tools/nrl_k8s/README.md +++ b/infra/nrl_k8s/README.md @@ -35,20 +35,20 @@ for both setup and development — it's what the project is tested with. ### End-user install (global `nrl-k8s` binary) ```bash -uv tool install ./tools/nrl_k8s +uv tool install ./infra/nrl_k8s nrl-k8s --version ``` `uv tool install` drops the CLI in `~/.local/bin` (on `PATH`) inside its own isolated environment, so it never clashes with whatever your project venv has pinned. Upgrade with `uv tool upgrade nrl-k8s` after a git pull, -or `uv tool install --reinstall ./tools/nrl_k8s`. +or `uv tool install --reinstall ./infra/nrl_k8s`. ### Development (editable install + tests) ```bash # from the repo root -cd tools/nrl_k8s +cd infra/nrl_k8s uv venv # creates .venv/ source .venv/bin/activate uv pip install -e ".[test]" # editable install + test extras @@ -58,8 +58,8 @@ pytest # 9 test modules, ~100 tests Or run commands without activating the venv: ```bash -uv run --directory tools/nrl_k8s -- pytest -uv run --directory tools/nrl_k8s -- nrl-k8s --help +uv run --directory infra/nrl_k8s -- pytest +uv run --directory infra/nrl_k8s -- nrl-k8s --help ``` The package depends on `click`, `omegaconf`, `pydantic`, `kubernetes`, @@ -70,7 +70,7 @@ Ray's Job SDK and runs the training entrypoint inside the cluster image. ## Quick start Three canonical flows ship with working recipes under -`tools/nrl_k8s/examples/`. All three train Qwen3-4B with GRPO on the +`infra/nrl_k8s/examples/`. All three train Qwen3-4B with GRPO on the `instruction_following` gym; they differ in how many RayClusters the run occupies and where generation/gym live. @@ -88,8 +88,8 @@ between roles, no endpoint-registry rendezvous. ```bash nrl-k8s run \ - tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ + infra/nrl_k8s/examples/qwen3_4b_if_single.yaml \ + --infra infra/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml \ --wait ``` @@ -100,8 +100,8 @@ RayCluster runs the gym rollout server. ```bash nrl-k8s run \ - tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml \ + infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ + --infra infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml \ --raycluster --wait ``` @@ -112,8 +112,8 @@ training cluster and tails its logs. ```bash nrl-k8s status \ - tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml + infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ + --infra infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml ``` ### `qwen3_4b_if_full_disagg` — generation + gym + training on separate clusters @@ -123,8 +123,8 @@ the standalone generation server, which lives on its own GPUs: ```bash nrl-k8s run \ - tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ + infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ --raycluster --wait ``` @@ -136,8 +136,8 @@ Once the training Ray Job is submitted its auto-generated ID is printed and ```bash nrl-k8s status \ - tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml + infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml ``` `DAEMON` is populated for `generation` and `gym`; `training` shows `—` @@ -156,7 +156,7 @@ Each run is two files: a recipe and an infra. - **`.infra.yaml`** — K8s-only. Namespace, container image, the inline RayCluster spec for each role, daemon entrypoints, the training entrypoint, and where Ray should upload code from. Validated against - `nrl_k8s.schema.InfraConfig` (see `tools/nrl_k8s/src/nrl_k8s/schema.py`). + `nrl_k8s.schema.InfraConfig` (see `infra/nrl_k8s/src/nrl_k8s/schema.py`). You can also bundle the two in one file — put an `infra:` top-level key on the recipe and omit `--infra`. The split is preferred for anything you plan @@ -168,7 +168,7 @@ Recipes support a `defaults:` field (same semantics as NeMo-RL's own loader). Point it at a parent recipe path relative to the file itself: ```yaml -# tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml +# infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml defaults: ../../../examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml grpo: @@ -177,7 +177,7 @@ grpo: The parent is loaded first; the child's keys are then merged on top. Chains work — the parent can itself have a `defaults:`. See -`tools/nrl_k8s/src/nrl_k8s/config.py:165` for the walker. +`infra/nrl_k8s/src/nrl_k8s/config.py:165` for the walker. Infra files also honour `defaults:` (via the same walker), so a team can keep a `defaults.infra.yaml` with shared node selectors, image, and @@ -187,7 +187,7 @@ namespace and point each per-run infra at it. Four layers stack low-to-high (last wins): -1. Shipped defaults: `tools/nrl_k8s/src/nrl_k8s/defaults/defaults.example.yaml` +1. Shipped defaults: `infra/nrl_k8s/src/nrl_k8s/defaults/defaults.example.yaml` 2. User defaults: `~/.config/nrl-k8s/defaults.yaml` (optional; can be repointed with `NRL_K8S_DEFAULTS=/path/to/file.yaml`) 3. The infra file (via `--infra`) *or* the recipe's `infra:` block. Not both. @@ -195,7 +195,7 @@ Four layers stack low-to-high (last wins): `grpo.max_num_steps=10`. `infra.*` overrides target the infra layer; everything else targets the -recipe. See `tools/nrl_k8s/src/nrl_k8s/config.py:102` for the partition +recipe. See `infra/nrl_k8s/src/nrl_k8s/config.py:102` for the partition logic. ## Command reference @@ -214,8 +214,8 @@ the format picks up from the extension (`.yaml` / `.json`). ```bash # summary nrl-k8s check \ - tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml + infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml # full bundle for diffs / kubectl apply --dry-run piping nrl-k8s check ... -o /tmp/bundle.yaml @@ -234,8 +234,8 @@ manifest that would be applied and exits without hitting the API server: ```bash nrl-k8s cluster up \ - tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ + infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ --role training --dry-run ``` @@ -264,8 +264,8 @@ Delete a RayCluster by role (resolved from the recipe) or by name. ```bash nrl-k8s cluster down \ - tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ + infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ --role gym ``` @@ -292,8 +292,8 @@ daemon's Ray Job when the role has one, else the head pod's container logs. Override with `--source {daemon,head,worker}`. ```bash -nrl-k8s logs tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ +nrl-k8s logs infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ --role generation -f --tail 500 ``` @@ -303,8 +303,8 @@ List Ray Jobs currently on the role's RayCluster (via its dashboard). ```bash nrl-k8s job list \ - tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ - --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ + infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ + --infra infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml \ --role training ``` @@ -402,8 +402,8 @@ submission goes through `kubectl exec` and code comes from `/opt/nemo-rl` inside the container instead of a laptop upload. ```bash -RECIPE=tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml -INFRA=tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml +RECIPE=infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml +INFRA=infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml # Admin, once: bring up the two RayClusters. nrl-k8s cluster up "$RECIPE" --infra "$INFRA" --role gym @@ -537,7 +537,7 @@ etc.) — the CLI does not inject `cd` for you. idempotency-relevant actions before submitting: 1. **Endpoint registry reset.** The CLI parses the gym daemon's - `--job-id` flag (see `tools/nrl_k8s/src/nrl_k8s/orchestrate.py:231`) and + `--job-id` flag (see `infra/nrl_k8s/src/nrl_k8s/orchestrate.py:231`) and deletes the `nemo-rl-endpoints-` ConfigMap. Without this the new gym or training publishes alongside stale keys from a prior failed run, and the rendezvous picks up stragglers. See the recipes guide for the @@ -569,7 +569,7 @@ isn't included (see `qwen3_4b_if_full_disagg.infra.yaml:229`). If uploads are sl `nrl-k8s check` won't help — instead `ls -lh` the staged tmpdir by running `nrl-k8s run --raycluster --wait` and inspecting the log line `[training] staging working_dir ...` (look at -`tools/nrl_k8s/src/nrl_k8s/workdir.py` for defaults). +`infra/nrl_k8s/src/nrl_k8s/workdir.py` for defaults). ### "expired token" / Kubernetes SSO errors diff --git a/tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml b/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml similarity index 96% rename from tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml rename to infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml index ccacc65e6fe..fd713eea986 100644 --- a/tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml +++ b/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml @@ -25,13 +25,13 @@ # Usage: # # Long-lived cluster — idempotent; reuses the live cluster if it # # already matches, applies if absent. Submits training per invocation. -# nrl-k8s run tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml \ +# nrl-k8s run infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml \ +# --infra infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml \ # --run-id qwen3-30b-math-$(date +%Y%m%d-%H%M%S) # # # One-shot with auto-teardown (ephemeral RayCluster, no cleanup needed): -# nrl-k8s rayjob tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml +# nrl-k8s rayjob infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml \ +# --infra infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml _shared: headNodeSelector: &head_node_selector @@ -135,7 +135,7 @@ launch: mkdir -p "\${LOG_DIR}" python -u examples/run_grpo.py \ - --config tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml \ + --config infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml \ logger.wandb_enabled=true \ logger.wandb.project=nemorl-single-k8s \ logger.wandb.name=qwen3-30b-math-gb300 \ diff --git a/tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml b/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml similarity index 100% rename from tools/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml rename to infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml diff --git a/tools/nrl_k8s/pyproject.toml b/infra/nrl_k8s/pyproject.toml similarity index 100% rename from tools/nrl_k8s/pyproject.toml rename to infra/nrl_k8s/pyproject.toml diff --git a/tools/nrl_k8s/src/nrl_k8s/__init__.py b/infra/nrl_k8s/src/nrl_k8s/__init__.py similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/__init__.py rename to infra/nrl_k8s/src/nrl_k8s/__init__.py diff --git a/tools/nrl_k8s/src/nrl_k8s/_logging.py b/infra/nrl_k8s/src/nrl_k8s/_logging.py similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/_logging.py rename to infra/nrl_k8s/src/nrl_k8s/_logging.py diff --git a/tools/nrl_k8s/src/nrl_k8s/_retry.py b/infra/nrl_k8s/src/nrl_k8s/_retry.py similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/_retry.py rename to infra/nrl_k8s/src/nrl_k8s/_retry.py diff --git a/tools/nrl_k8s/src/nrl_k8s/cli.py b/infra/nrl_k8s/src/nrl_k8s/cli.py similarity index 99% rename from tools/nrl_k8s/src/nrl_k8s/cli.py rename to infra/nrl_k8s/src/nrl_k8s/cli.py index 5456671f291..eafba6ad512 100644 --- a/tools/nrl_k8s/src/nrl_k8s/cli.py +++ b/infra/nrl_k8s/src/nrl_k8s/cli.py @@ -323,7 +323,7 @@ def _dump_check_output( # ============================================================================= # Deprecated aliases — kept only where scripts/docs still reference them. # Unimplemented stub commands (doctor/dashboard/dev) were removed; see -# tools/nrl_k8s/docs/roadmap.md for the planned work. +# infra/nrl_k8s/docs/roadmap.md for the planned work. # ============================================================================= diff --git a/tools/nrl_k8s/src/nrl_k8s/config.py b/infra/nrl_k8s/src/nrl_k8s/config.py similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/config.py rename to infra/nrl_k8s/src/nrl_k8s/config.py diff --git a/tools/nrl_k8s/src/nrl_k8s/defaults/defaults.example.yaml b/infra/nrl_k8s/src/nrl_k8s/defaults/defaults.example.yaml similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/defaults/defaults.example.yaml rename to infra/nrl_k8s/src/nrl_k8s/defaults/defaults.example.yaml diff --git a/tools/nrl_k8s/src/nrl_k8s/dev.py b/infra/nrl_k8s/src/nrl_k8s/dev.py similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/dev.py rename to infra/nrl_k8s/src/nrl_k8s/dev.py diff --git a/tools/nrl_k8s/src/nrl_k8s/inspect.py b/infra/nrl_k8s/src/nrl_k8s/inspect.py similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/inspect.py rename to infra/nrl_k8s/src/nrl_k8s/inspect.py diff --git a/tools/nrl_k8s/src/nrl_k8s/k8s.py b/infra/nrl_k8s/src/nrl_k8s/k8s.py similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/k8s.py rename to infra/nrl_k8s/src/nrl_k8s/k8s.py diff --git a/tools/nrl_k8s/src/nrl_k8s/manifest.py b/infra/nrl_k8s/src/nrl_k8s/manifest.py similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/manifest.py rename to infra/nrl_k8s/src/nrl_k8s/manifest.py diff --git a/tools/nrl_k8s/src/nrl_k8s/orchestrate.py b/infra/nrl_k8s/src/nrl_k8s/orchestrate.py similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/orchestrate.py rename to infra/nrl_k8s/src/nrl_k8s/orchestrate.py diff --git a/tools/nrl_k8s/src/nrl_k8s/rayjob.py b/infra/nrl_k8s/src/nrl_k8s/rayjob.py similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/rayjob.py rename to infra/nrl_k8s/src/nrl_k8s/rayjob.py diff --git a/tools/nrl_k8s/src/nrl_k8s/schema.py b/infra/nrl_k8s/src/nrl_k8s/schema.py similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/schema.py rename to infra/nrl_k8s/src/nrl_k8s/schema.py diff --git a/tools/nrl_k8s/src/nrl_k8s/submit.py b/infra/nrl_k8s/src/nrl_k8s/submit.py similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/submit.py rename to infra/nrl_k8s/src/nrl_k8s/submit.py diff --git a/tools/nrl_k8s/src/nrl_k8s/submitters/__init__.py b/infra/nrl_k8s/src/nrl_k8s/submitters/__init__.py similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/submitters/__init__.py rename to infra/nrl_k8s/src/nrl_k8s/submitters/__init__.py diff --git a/tools/nrl_k8s/src/nrl_k8s/submitters/exec_.py b/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/submitters/exec_.py rename to infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py diff --git a/tools/nrl_k8s/src/nrl_k8s/submitters/portforward.py b/infra/nrl_k8s/src/nrl_k8s/submitters/portforward.py similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/submitters/portforward.py rename to infra/nrl_k8s/src/nrl_k8s/submitters/portforward.py diff --git a/tools/nrl_k8s/src/nrl_k8s/workdir.py b/infra/nrl_k8s/src/nrl_k8s/workdir.py similarity index 100% rename from tools/nrl_k8s/src/nrl_k8s/workdir.py rename to infra/nrl_k8s/src/nrl_k8s/workdir.py diff --git a/tools/nrl_k8s/tests/__init__.py b/infra/nrl_k8s/tests/__init__.py similarity index 100% rename from tools/nrl_k8s/tests/__init__.py rename to infra/nrl_k8s/tests/__init__.py diff --git a/tools/nrl_k8s/tests/unit/__init__.py b/infra/nrl_k8s/tests/unit/__init__.py similarity index 100% rename from tools/nrl_k8s/tests/unit/__init__.py rename to infra/nrl_k8s/tests/unit/__init__.py diff --git a/tools/nrl_k8s/tests/unit/test_cli.py b/infra/nrl_k8s/tests/unit/test_cli.py similarity index 100% rename from tools/nrl_k8s/tests/unit/test_cli.py rename to infra/nrl_k8s/tests/unit/test_cli.py diff --git a/tools/nrl_k8s/tests/unit/test_config.py b/infra/nrl_k8s/tests/unit/test_config.py similarity index 100% rename from tools/nrl_k8s/tests/unit/test_config.py rename to infra/nrl_k8s/tests/unit/test_config.py diff --git a/tools/nrl_k8s/tests/unit/test_inspect.py b/infra/nrl_k8s/tests/unit/test_inspect.py similarity index 100% rename from tools/nrl_k8s/tests/unit/test_inspect.py rename to infra/nrl_k8s/tests/unit/test_inspect.py diff --git a/tools/nrl_k8s/tests/unit/test_k8s.py b/infra/nrl_k8s/tests/unit/test_k8s.py similarity index 100% rename from tools/nrl_k8s/tests/unit/test_k8s.py rename to infra/nrl_k8s/tests/unit/test_k8s.py diff --git a/tools/nrl_k8s/tests/unit/test_manifest.py b/infra/nrl_k8s/tests/unit/test_manifest.py similarity index 100% rename from tools/nrl_k8s/tests/unit/test_manifest.py rename to infra/nrl_k8s/tests/unit/test_manifest.py diff --git a/tools/nrl_k8s/tests/unit/test_orchestrate.py b/infra/nrl_k8s/tests/unit/test_orchestrate.py similarity index 100% rename from tools/nrl_k8s/tests/unit/test_orchestrate.py rename to infra/nrl_k8s/tests/unit/test_orchestrate.py diff --git a/tools/nrl_k8s/tests/unit/test_rayjob.py b/infra/nrl_k8s/tests/unit/test_rayjob.py similarity index 100% rename from tools/nrl_k8s/tests/unit/test_rayjob.py rename to infra/nrl_k8s/tests/unit/test_rayjob.py diff --git a/tools/nrl_k8s/tests/unit/test_schema.py b/infra/nrl_k8s/tests/unit/test_schema.py similarity index 100% rename from tools/nrl_k8s/tests/unit/test_schema.py rename to infra/nrl_k8s/tests/unit/test_schema.py diff --git a/tools/nrl_k8s/tests/unit/test_submit.py b/infra/nrl_k8s/tests/unit/test_submit.py similarity index 100% rename from tools/nrl_k8s/tests/unit/test_submit.py rename to infra/nrl_k8s/tests/unit/test_submit.py diff --git a/tools/nrl_k8s/tests/unit/test_submitters.py b/infra/nrl_k8s/tests/unit/test_submitters.py similarity index 100% rename from tools/nrl_k8s/tests/unit/test_submitters.py rename to infra/nrl_k8s/tests/unit/test_submitters.py diff --git a/tools/nrl_k8s/tests/unit/test_workdir.py b/infra/nrl_k8s/tests/unit/test_workdir.py similarity index 100% rename from tools/nrl_k8s/tests/unit/test_workdir.py rename to infra/nrl_k8s/tests/unit/test_workdir.py diff --git a/infra/nrl_k8s/uv.lock b/infra/nrl_k8s/uv.lock new file mode 100644 index 00000000000..d33f381003f --- /dev/null +++ b/infra/nrl_k8s/uv.lock @@ -0,0 +1,2310 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version < '3.12'", +] + +[manifest] + +[[manifest.dependency-metadata]] +name = "causal-conv1d" +version = "1.5.4" +requires-dist = ["torch", "packaging", "ninja"] + +[[manifest.dependency-metadata]] +name = "deep-ep" +version = "1.2.1+bfded34" +requires-dist = ["torch", "packaging", "ninja"] + +[[manifest.dependency-metadata]] +name = "deep-gemm" +version = "2.0.0+7b6b556" +requires-dist = ["torch", "packaging", "ninja"] + +[[manifest.dependency-metadata]] +name = "drain3" +version = "0.9.11" +requires-dist = ["jsonpickle", "cachetools>=4.2.1"] + +[[manifest.dependency-metadata]] +name = "flash-attn" +requires-dist = ["torch", "einops", "setuptools", "psutil", "ninja"] + +[[manifest.dependency-metadata]] +name = "logsage" +version = "0.1.5" +requires-dist = ["drain3>=0.9.11,<0.10.0", "langchain>=0.3.27,<0.4.0", "langchain-core>=0.3.0,<1.0.0", "langchain-nvidia-ai-endpoints>=0.3.18,<0.4.0", "nh3>=0.3.1,<0.4.0", "numpy", "pandas", "pydantic-settings>=2.11.0,<3.0.0", "requests>=2.32.5,<3.0.0"] + +[[manifest.dependency-metadata]] +name = "mamba-ssm" +version = "2.2.6.post3" +requires-dist = ["torch", "packaging", "ninja", "causal-conv1d"] + +[[manifest.dependency-metadata]] +name = "megatron-bridge" +version = "0.0.0" +requires-dist = ["transformers>=5.0.0,<=5.3.0", "peft>=0.18.1", "datasets>=2.20.0", "accelerate", "diffusers>=0.36.0", "peft>=0.18.0", "einops", "imageio", "imageio-ffmpeg", "omegaconf>=2.3.0", "tensorboard>=2.19.0", "typing-extensions", "rich", "wandb>=0.25.0", "six>=1.17.0", "regex>=2024.11.6", "pyyaml>=6.0.2", "tqdm>=4.67.1", "hydra-core>1.3,<=1.3.2", "qwen-vl-utils", "transformer-engine[pytorch,core-cu12]", "mamba-ssm", "nvidia-resiliency-ext", "causal-conv1d", "flash-linear-attention", "timm", "open-clip-torch>=3.2.0", "mlflow>=3.9.0", "comet-ml>=3.50.0", "torch>=2.6.0"] + +[[manifest.dependency-metadata]] +name = "nv-grouped-gemm" +version = "1.1.4.post7" +requires-dist = ["setuptools", "wheel", "torch", "numpy"] + +[[manifest.dependency-metadata]] +name = "sglang" +version = "0.5.10" +requires-dist = ["ipython", "aiohttp", "apache-tvm-ffi>=0.1.5,<0.2", "anthropic>=0.20.0", "blobfile==3.0.0", "build", "compressed-tensors", "cuda-python==12.9", "decord2", "datasets", "einops", "fastapi", "flashinfer-python==0.6.7.post2", "flashinfer-cubin==0.6.7.post2", "gguf", "interegular", "llguidance>=0.7.11,<0.8.0", "modelscope", "msgspec", "ninja", "numpy", "nvidia-cutlass-dsl>=4.4.1", "nvidia-ml-py", "openai-harmony==0.0.4", "openai==2.6.1", "orjson", "outlines==0.1.11", "packaging", "partial-json-parser", "pillow", "prometheus-client>=0.20.0", "psutil", "py-spy", "pybase64", "pydantic", "python-multipart", "pyzmq>=25.1.2", "quack-kernels>=0.3.0", "requests", "scipy", "sentencepiece", "setproctitle", "flash-attn-4>=4.0.0b4", "sglang-kernel==0.4.1", "soundfile==0.13.1", "tiktoken", "timm==1.0.16", "torch-memory-saver==0.0.9", "torch==2.9.1", "torchao==0.9.0", "torchaudio==2.9.1", "torchcodec==0.9.1 ; (platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l') or sys_platform != 'linux'", "torchvision", "tqdm", "mistral-common>=1.9.0", "transformers==5.3.0", "uvicorn", "uvloop", "watchfiles", "xgrammar==0.1.32", "smg-grpc-servicer>=0.5.0"] + +[[manifest.dependency-metadata]] +name = "sglang-kernel" +version = "0.4.1" +requires-dist = ["torch", "scikit-build-core", "wheel"] + +[[manifest.dependency-metadata]] +name = "transformer-engine" +version = "2.14.0+71bbefb" +requires-dist = ["torch", "pydantic", "importlib-metadata>=1.0", "packaging"] + +[[manifest.dependency-metadata]] +name = "transformer-engine-torch" +version = "2.14.0+71bbefb" +requires-dist = ["torch", "transformer-engine"] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/85/cebc47ee74d8b408749073a1a46c6fcba13d170dc8af7e61996c6c9394ac/aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b", size = 750547, upload-time = "2026-03-31T21:56:30.024Z" }, + { url = "https://files.pythonhosted.org/packages/05/98/afd308e35b9d3d8c9ec54c0918f1d722c86dc17ddfec272fcdbcce5a3124/aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5", size = 503535, upload-time = "2026-03-31T21:56:31.935Z" }, + { url = "https://files.pythonhosted.org/packages/6f/4d/926c183e06b09d5270a309eb50fbde7b09782bfd305dec1e800f329834fb/aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670", size = 497830, upload-time = "2026-03-31T21:56:33.654Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d6/f47d1c690f115a5c2a5e8938cce4a232a5be9aac5c5fb2647efcbbbda333/aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274", size = 1682474, upload-time = "2026-03-31T21:56:35.513Z" }, + { url = "https://files.pythonhosted.org/packages/01/44/056fd37b1bb52eac760303e5196acc74d9d546631b035704ae5927f7b4ac/aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a", size = 1655259, upload-time = "2026-03-31T21:56:37.843Z" }, + { url = "https://files.pythonhosted.org/packages/91/9f/78eb1a20c1c28ae02f6a3c0f4d7b0dcc66abce5290cadd53d78ce3084175/aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d", size = 1736204, upload-time = "2026-03-31T21:56:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/de/6c/d20d7de23f0b52b8c1d9e2033b2db1ac4dacbb470bb74c56de0f5f86bb4f/aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796", size = 1826198, upload-time = "2026-03-31T21:56:41.378Z" }, + { url = "https://files.pythonhosted.org/packages/2f/86/a6f3ff1fd795f49545a7c74b2c92f62729135d73e7e4055bf74da5a26c82/aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95", size = 1681329, upload-time = "2026-03-31T21:56:43.374Z" }, + { url = "https://files.pythonhosted.org/packages/fb/68/84cd3dab6b7b4f3e6fe9459a961acb142aaab846417f6e8905110d7027e5/aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5", size = 1560023, upload-time = "2026-03-31T21:56:45.031Z" }, + { url = "https://files.pythonhosted.org/packages/41/2c/db61b64b0249e30f954a65ab4cb4970ced57544b1de2e3c98ee5dc24165f/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a", size = 1652372, upload-time = "2026-03-31T21:56:47.075Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/e96988a6c982d047810c772e28c43c64c300c943b0ed5c1c0c4ce1e1027c/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73", size = 1662031, upload-time = "2026-03-31T21:56:48.835Z" }, + { url = "https://files.pythonhosted.org/packages/b7/26/a56feace81f3d347b4052403a9d03754a0ab23f7940780dada0849a38c92/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297", size = 1708118, upload-time = "2026-03-31T21:56:50.833Z" }, + { url = "https://files.pythonhosted.org/packages/78/6e/b6173a8ff03d01d5e1a694bc06764b5dad1df2d4ed8f0ceec12bb3277936/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074", size = 1548667, upload-time = "2026-03-31T21:56:52.81Z" }, + { url = "https://files.pythonhosted.org/packages/16/13/13296ffe2c132d888b3fe2c195c8b9c0c24c89c3fa5cc2c44464dc23b22e/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e", size = 1724490, upload-time = "2026-03-31T21:56:54.541Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1f1c287f4a79782ef36e5a6e62954c85343bc30470d862d30bd5f26c9fa2/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7", size = 1667109, upload-time = "2026-03-31T21:56:56.21Z" }, + { url = "https://files.pythonhosted.org/packages/ef/42/8461a2aaf60a8f4ea4549a4056be36b904b0eb03d97ca9a8a2604681a500/aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9", size = 439478, upload-time = "2026-03-31T21:56:58.292Z" }, + { url = "https://files.pythonhosted.org/packages/e5/71/06956304cb5ee439dfe8d86e1b2e70088bd88ed1ced1f42fb29e5d855f0e/aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76", size = 462047, upload-time = "2026-03-31T21:57:00.257Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, + { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, + { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, + { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, + { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, + { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, + { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, + { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, +] + +[[package]] +name = "aiohttp-cors" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/d89e846a5444b3d5eb8985a6ddb0daef3774928e1bfbce8e84ec97b0ffa7/aiohttp_cors-0.8.1.tar.gz", hash = "sha256:ccacf9cb84b64939ea15f859a146af1f662a6b1d68175754a07315e305fb1403", size = 38626, upload-time = "2025-03-31T14:16:20.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/3b/40a68de458904bcc143622015fff2352b6461cd92fd66d3527bf1c6f5716/aiohttp_cors-0.8.1-py3-none-any.whl", hash = "sha256:3180cf304c5c712d626b9162b195b1db7ddf976a2a25172b35bb2448b890a80d", size = 25231, upload-time = "2025-03-31T14:16:18.478Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +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-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[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 = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[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 = "colorful" +version = "0.5.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/31/109ef4bedeb32b4202e02ddb133162457adc4eb890a9ed9c05c9dd126ed0/colorful-0.5.8.tar.gz", hash = "sha256:bb16502b198be2f1c42ba3c52c703d5f651d826076817185f0294c1a549a7445", size = 209361, upload-time = "2025-10-29T11:53:21.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/11/25cdf9d5fc21efd30134fc74c43702c6f7ef09ebae8ed927f1283403ad8d/colorful-0.5.8-py2.py3-none-any.whl", hash = "sha256:a9381fdda3337fbaba5771991020abc69676afa102646650b759927892875992", size = 201334, upload-time = "2025-10-29T11:53:20.251Z" }, +] + +[[package]] +name = "cryptography" +version = "47.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/98/40dfe932134bdcae4f6ab5927c87488754bf9eb79297d7e0070b78dd58e9/cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", size = 7912214, upload-time = "2026-04-24T19:53:03.864Z" }, + { url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" }, + { url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5e/13ed0cdd0eb88ba159d6dd5ebfece8cb901dbcf1ae5ac4072e28b55d3153/cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", size = 5252906, upload-time = "2026-04-24T19:53:13.532Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" }, + { url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" }, + { url = "https://files.pythonhosted.org/packages/86/53/5395d944dfd48cb1f67917f533c609c34347185ef15eb4308024c876f274/cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", size = 5207817, upload-time = "2026-04-24T19:53:22.498Z" }, + { url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/54/ed/5f524db1fade9c013aa618e1c99c6ed05e8ffc9ceee6cda22fed22dda3f4/cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", size = 3258581, upload-time = "2026-04-24T19:53:31.058Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/1b901990b174786569029f67542b3edf72ac068b6c3c8683c17e6a2f5363/cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", size = 3775309, upload-time = "2026-04-24T19:53:33.054Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/7aa18ad9c11bc87689affa5ce4368d884b517502d75739d475fc6f4a03c7/cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", size = 7904299, upload-time = "2026-04-24T19:53:35.003Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" }, + { url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/300327b0a47f6dc94dd8b71b57052aefe178bb51745073d73d80604f11ab/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", size = 5238019, upload-time = "2026-04-24T19:53:44.577Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d7/0b3c71090a76e5c203164a47688b697635ece006dcd2499ab3a4dbd3f0bd/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736", size = 5194988, upload-time = "2026-04-24T19:53:52.962Z" }, + { url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" }, + { url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" }, + { url = "https://files.pythonhosted.org/packages/31/98/dc4ad376ac5f1a1a7d4a83f7b0c6f2bcad36b5d2d8f30aeb482d3a7d9582/cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", size = 3237158, upload-time = "2026-04-24T19:54:02.606Z" }, + { url = "https://files.pythonhosted.org/packages/bc/da/97f62d18306b5133468bc3f8cc73a3111e8cdc8cf8d3e69474d6e5fd2d1b/cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", size = 3758706, upload-time = "2026-04-24T19:54:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e0/34/a4fae8ae7c3bc227460c9ae43f56abf1b911da0ec29e0ebac53bb0a4b6b7/cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", size = 7904072, upload-time = "2026-04-24T19:54:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" }, + { url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/2c5fbeea70adbbca2bbae865e1d605d6a4a7f8dbd9d33eaf69645087f06c/cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", size = 5225777, upload-time = "2026-04-24T19:54:15.18Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/a5/51/661cbee74f594c5d97ff82d34f10d5551c085ca4668645f4606ebd22bd5d/cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", size = 5181411, upload-time = "2026-04-24T19:54:24.376Z" }, + { url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/bd/0a9d3edbf5eadbac926d7b9b3cd0c4be584eeeae4a003d24d9eda4affbbd/cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", size = 3248487, upload-time = "2026-04-24T19:54:33.494Z" }, + { url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a0/928c9ce0d120a40a81aa99e3ba383e87337b9ac9ef9f6db02e4d7822424d/cryptography-47.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", size = 3909893, upload-time = "2026-04-24T19:54:38.334Z" }, + { url = "https://files.pythonhosted.org/packages/81/75/d691e284750df5d9569f2b1ce4a00a71e1d79566da83b2b3e5549c84917f/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", size = 4587867, upload-time = "2026-04-24T19:54:40.619Z" }, + { url = "https://files.pythonhosted.org/packages/07/d6/1b90f1a4e453009730b4545286f0b39bb348d805c11181fc31544e4f9a65/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", size = 4627192, upload-time = "2026-04-24T19:54:42.849Z" }, + { url = "https://files.pythonhosted.org/packages/dc/53/cb358a80e9e359529f496870dd08c102aa8a4b5b9f9064f00f0d6ed5b527/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", size = 4587486, upload-time = "2026-04-24T19:54:44.908Z" }, + { url = "https://files.pythonhosted.org/packages/8b/57/aaa3d53876467a226f9a7a82fd14dd48058ad2de1948493442dfa16e2ffd/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", size = 4626327, upload-time = "2026-04-24T19:54:47.813Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9c/51f28c3550276bcf35660703ba0ab829a90b88be8cd98a71ef23c2413913/cryptography-47.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", size = 3698916, upload-time = "2026-04-24T19:54:49.782Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[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 = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[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/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { 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/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { 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 = "google-api-core" +version = "2.30.3" +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/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/15/e56f351cf6ef1cfea58e6ac226a7318ed1deb2218c4b3cc9bd9e4b786c5a/google_api_core-2.30.3-py3-none-any.whl", hash = "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", size = 173274, upload-time = "2026-04-09T22:57:16.198Z" }, +] + +[[package]] +name = "google-auth" +version = "2.49.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/fc/e925290a1ad95c975c459e2df070fac2b90954e13a0370ac505dff78cb99/google_auth-2.49.2.tar.gz", hash = "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", size = 333958, upload-time = "2026-04-10T00:41:21.888Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl", hash = "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5", size = 240638, upload-time = "2026-04-10T00:41:14.501Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.74.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload-time = "2026-04-02T21:23:26.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload-time = "2026-04-02T21:22:49.108Z" }, +] + +[[package]] +name = "grpcio" +version = "1.80.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/cd/bb7b7e54084a344c03d68144450da7ddd5564e51a298ae1662de65f48e2d/grpcio-1.80.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:886457a7768e408cdce226ad1ca67d2958917d306523a0e21e1a2fdaa75c9c9c", size = 6050363, upload-time = "2026-03-30T08:46:20.894Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/1417f5c3460dea65f7a2e3c14e8b31e77f7ffb730e9bfadd89eda7a9f477/grpcio-1.80.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:7b641fc3f1dc647bfd80bd713addc68f6d145956f64677e56d9ebafc0bd72388", size = 12026037, upload-time = "2026-03-30T08:46:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/43/98/c910254eedf2cae368d78336a2de0678e66a7317d27c02522392f949b5c6/grpcio-1.80.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33eb763f18f006dc7fee1e69831d38d23f5eccd15b2e0f92a13ee1d9242e5e02", size = 6602306, upload-time = "2026-03-30T08:46:27.593Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f8/88ca4e78c077b2b2113d95da1e1ab43efd43d723c9a0397d26529c2c1a56/grpcio-1.80.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:52d143637e3872633fc7dd7c3c6a1c84e396b359f3a72e215f8bf69fd82084fc", size = 7301535, upload-time = "2026-03-30T08:46:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f9/96/f28660fe2fe0f153288bf4a04e4910b7309d442395135c88ed4f5b3b8b40/grpcio-1.80.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c51bf8ac4575af2e0678bccfb07e47321fc7acb5049b4482832c5c195e04e13a", size = 6808669, upload-time = "2026-03-30T08:46:31.984Z" }, + { url = "https://files.pythonhosted.org/packages/47/eb/3f68a5e955779c00aeef23850e019c1c1d0e032d90633ba49c01ad5a96e0/grpcio-1.80.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:50a9871536d71c4fba24ee856abc03a87764570f0c457dd8db0b4018f379fed9", size = 7409489, upload-time = "2026-03-30T08:46:34.684Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a7/d2f681a4bfb881be40659a309771f3bdfbfdb1190619442816c3f0ffc079/grpcio-1.80.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a72d84ad0514db063e21887fbacd1fd7acb4d494a564cae22227cd45c7fbf199", size = 8423167, upload-time = "2026-03-30T08:46:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/29b4589c204959aa35ce5708400a05bba72181807c45c47b3ec000c39333/grpcio-1.80.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7691a6788ad9196872f95716df5bc643ebba13c97140b7a5ee5c8e75d1dea81", size = 7846761, upload-time = "2026-03-30T08:46:40.091Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d2/ed143e097230ee121ac5848f6ff14372dba91289b10b536d54fb1b7cbae7/grpcio-1.80.0-cp310-cp310-win32.whl", hash = "sha256:46c2390b59d67f84e882694d489f5b45707c657832d7934859ceb8c33f467069", size = 4156534, upload-time = "2026-03-30T08:46:42.026Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c9/df8279bb49b29409995e95efa85b72973d62f8aeff89abee58c91f393710/grpcio-1.80.0-cp310-cp310-win_amd64.whl", hash = "sha256:dc053420fc75749c961e2a4c906398d7c15725d36ccc04ae6d16093167223b58", size = 4889869, upload-time = "2026-03-30T08:46:44.219Z" }, + { url = "https://files.pythonhosted.org/packages/5d/db/1d56e5f5823257b291962d6c0ce106146c6447f405b60b234c4f222a7cde/grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a", size = 6055009, upload-time = "2026-03-30T08:46:46.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/18/c83f3cad64c5ca63bca7e91e5e46b0d026afc5af9d0a9972472ceba294b3/grpcio-1.80.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060", size = 12035295, upload-time = "2026-03-30T08:46:49.099Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8e/e14966b435be2dda99fbe89db9525ea436edc79780431a1c2875a3582644/grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2", size = 6610297, upload-time = "2026-03-30T08:46:52.123Z" }, + { url = "https://files.pythonhosted.org/packages/cc/26/d5eb38f42ce0e3fdc8174ea4d52036ef8d58cc4426cb800f2610f625dd75/grpcio-1.80.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cb8130ba457d2aa09fa6b7c3ed6b6e4e6a2685fce63cb803d479576c4d80e21", size = 7300208, upload-time = "2026-03-30T08:46:54.859Z" }, + { url = "https://files.pythonhosted.org/packages/25/51/bd267c989f85a17a5b3eea65a6feb4ff672af41ca614e5a0279cc0ea381c/grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab", size = 6813442, upload-time = "2026-03-30T08:46:57.056Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d9/d80eef735b19e9169e30164bbf889b46f9df9127598a83d174eb13a48b26/grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", size = 7414743, upload-time = "2026-03-30T08:46:59.682Z" }, + { url = "https://files.pythonhosted.org/packages/de/f2/567f5bd5054398ed6b0509b9a30900376dcf2786bd936812098808b49d8d/grpcio-1.80.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8502122a3cc1714038e39a0b071acb1207ca7844208d5ea0d091317555ee7106", size = 8426046, upload-time = "2026-03-30T08:47:02.474Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/73ef0141b4732ff5eacd68430ff2512a65c004696997f70476a83e548e7e/grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6", size = 7851641, upload-time = "2026-03-30T08:47:05.462Z" }, + { url = "https://files.pythonhosted.org/packages/46/69/abbfa360eb229a8623bab5f5a4f8105e445bd38ce81a89514ba55d281ad0/grpcio-1.80.0-cp311-cp311-win32.whl", hash = "sha256:51b4a7189b0bef2aa30adce3c78f09c83526cf3dddb24c6a96555e3b97340440", size = 4154368, upload-time = "2026-03-30T08:47:08.027Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d4/ae92206d01183b08613e846076115f5ac5991bae358d2a749fa864da5699/grpcio-1.80.0-cp311-cp311-win_amd64.whl", hash = "sha256:02e64bb0bb2da14d947a49e6f120a75e947250aebe65f9629b62bb1f5c14e6e9", size = 4894235, upload-time = "2026-03-30T08:47:10.839Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, + { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, + { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, + { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, + { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" }, + { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" }, + { url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" }, + { url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" }, + { url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" }, + { url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" }, +] + +[[package]] +name = "idna" +version = "3.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[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 = "35.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "durationpy" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/8f/85bf51ad4150f64e8c665daf0d9dfe9787ae92005efb9a4d1cba592bd79d/kubernetes-35.0.0.tar.gz", hash = "sha256:3d00d344944239821458b9efd484d6df9f011da367ecb155dadf9513f05f09ee", size = 1094642, upload-time = "2026-01-16T01:05:27.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/70/05b685ea2dffcb2adbf3cdcea5d8865b7bc66f67249084cf845012a0ff13/kubernetes-35.0.0-py2.py3-none-any.whl", hash = "sha256:39e2b33b46e5834ef6c3985ebfe2047ab39135d41de51ce7641a7ca5b372a13d", size = 2017602, upload-time = "2026-01-16T01:05:25.991Z" }, +] + +[[package]] +name = "msgpack" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" }, + { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" }, + { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" }, + { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, + { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, + { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, + { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, + { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, + { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, + { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, + { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, + { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +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/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, + { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, + { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, + { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, + { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { 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/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { 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 = "nrl-k8s" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "kubernetes" }, + { name = "omegaconf" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "ray", extra = ["default"] }, + { name = "tenacity" }, +] + +[package.optional-dependencies] +all = [ + { name = "pytest" }, + { name = "pytest-mock" }, +] +test = [ + { name = "pytest" }, + { name = "pytest-mock" }, +] + +[package.metadata] +requires-dist = [ + { name = "click", specifier = ">=8.1" }, + { name = "kubernetes", specifier = ">=29.0" }, + { name = "nrl-k8s", extras = ["test"], marker = "extra == 'all'" }, + { name = "omegaconf", specifier = ">=2.3" }, + { name = "pydantic", specifier = ">=2.5" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=7.4" }, + { name = "pytest-mock", marker = "extra == 'test'", specifier = ">=3.12" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "ray", extras = ["default"], specifier = ">=2.52" }, + { name = "tenacity", specifier = ">=8.2" }, +] +provides-extras = ["test", "all"] + +[[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 = "omegaconf" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, +] + +[[package]] +name = "opencensus" +version = "0.11.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "opencensus-context" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/a7/a46dcffa1b63084f9f17fe3c8cb20724c4c8f91009fd0b2cfdb27d5d2b35/opencensus-0.11.4.tar.gz", hash = "sha256:cbef87d8b8773064ab60e5c2a1ced58bbaa38a6d052c41aec224958ce544eff2", size = 64966, upload-time = "2024-01-03T18:04:07.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/ed/9fbdeb23a09e430d87b7d72d430484b88184633dc50f6bfb792354b6f661/opencensus-0.11.4-py2.py3-none-any.whl", hash = "sha256:a18487ce68bc19900336e0ff4655c5a116daf10c1b3685ece8d971bddad6a864", size = 128225, upload-time = "2024-01-03T18:04:05.127Z" }, +] + +[[package]] +name = "opencensus-context" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/96/3b6f638f6275a8abbd45e582448723bffa29c1fb426721dedb5c72f7d056/opencensus-context-0.1.3.tar.gz", hash = "sha256:a03108c3c10d8c80bb5ddf5c8a1f033161fa61972a9917f9b9b3a18517f0088c", size = 4066, upload-time = "2022-08-03T22:20:22.359Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/68/162c97ea78c957d68ecf78a5c5041d2e25bd5562bdf5d89a6cbf7f8429bf/opencensus_context-0.1.3-py2.py3-none-any.whl", hash = "sha256:073bb0590007af276853009fac7e4bab1d523c3f03baf4cb4511ca38967c6039", size = 5060, upload-time = "2022-08-03T22:20:20.352Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/fc/b7564cbef36601aef0d6c9bc01f7badb64be8e862c2e1c3c5c3b43b53e4f/opentelemetry_api-1.41.1.tar.gz", hash = "sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621", size = 71416, upload-time = "2026-04-24T13:15:38.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/59/3e7118ed140f76b0982ba4321bdaed1997a0473f9720de2d10788a577033/opentelemetry_api-1.41.1-py3-none-any.whl", hash = "sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f", size = 69007, upload-time = "2026-04-24T13:15:15.662Z" }, +] + +[[package]] +name = "opentelemetry-exporter-prometheus" +version = "0.62b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "prometheus-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/03/e1fbf14386ef171b949b71fba7c18643691a9390ab38c221df582e916569/opentelemetry_exporter_prometheus-0.62b1.tar.gz", hash = "sha256:7ecbac9aa76e7abb44082ab0ff2983e0a573e4091c4653f7db483b02bae03506", size = 15446, upload-time = "2026-04-24T13:15:43.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/d2/ee4002b88e20c59fae52bed008a63c6f7eff7d498f302032f6b0434a6de7/opentelemetry_exporter_prometheus-0.62b1-py3-none-any.whl", hash = "sha256:7a0b8a6402e107e1f93e38f074a668797e1103936b189561959531a67ffeba55", size = 13278, upload-time = "2026-04-24T13:15:22.485Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/e8/633c6d8a9c8840338b105907e55c32d3da1983abab5e52f899f72a82c3d1/opentelemetry_proto-1.41.1.tar.gz", hash = "sha256:4b9d2eb631237ea43b80e16c073af438554e32bc7e9e3f8ca4a9582f900020e5", size = 45670, upload-time = "2026-04-24T13:15:49.768Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/1e/5cd77035e3e82070e2265a63a760f715aacd3cb16dddc7efee913f297fcc/opentelemetry_proto-1.41.1-py3-none-any.whl", hash = "sha256:0496713b804d127a4147e32849fbaf5683fac8ee98550e8e7679cd706c289720", size = 72076, upload-time = "2026-04-24T13:15:32.542Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d0/54ee30dab82fb0acda23d144502771ff76ef8728459c83c3e89ef9fb1825/opentelemetry_sdk-1.41.1.tar.gz", hash = "sha256:724b615e1215b5aeacda0abb8a6a8922c9a1853068948bd0bd225a56d0c792e6", size = 230180, upload-time = "2026-04-24T13:15:50.991Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/e7/a1420b698aad018e1cf60fdbaaccbe49021fb415e2a0d81c242f4c518f54/opentelemetry_sdk-1.41.1-py3-none-any.whl", hash = "sha256:edee379c126c1bce952b0c812b48fe8ff35b30df0eecf17e98afa4d598b7d85d", size = 180213, upload-time = "2026-04-24T13:15:33.767Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.62b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/de/911ac9e309052aca1b20b2d5549d3db45d1011e1a610e552c6ccdd1b64f8/opentelemetry_semantic_conventions-0.62b1.tar.gz", hash = "sha256:c5cc6e04a7f8c7cdd30be2ed81499fa4e75bfbd52c9cb70d40af1f9cd3619802", size = 145750, upload-time = "2026-04-24T13:15:52.236Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/a6/83dc2ab6fa397ee66fba04fe2e74bdf7be3b3870005359ceb7689103c058/opentelemetry_semantic_conventions-0.62b1-py3-none-any.whl", hash = "sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c", size = 231620, upload-time = "2026-04-24T13:15:35.454Z" }, +] + +[[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 = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/0d/94dfe80193e79d55258345901acd2917523d56e8381bc4dee7fd38e3868a/proto_plus-1.27.2.tar.gz", hash = "sha256:b2adde53adadf75737c44d3dcb0104fde65250dfc83ad59168b4aa3e574b6a24", size = 57204, upload-time = "2026-03-26T22:18:57.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/f3/1fba73eeffafc998a25d59703b63f8be4fe8a5cb12eaff7386a0ba0f7125/proto_plus-1.27.2-py3-none-any.whl", hash = "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", size = 50450, upload-time = "2026-03-26T22:13:42.927Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "py-spy" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/93/d8/5b71371f50cf153b1307e5a11ac8a4ce4d85651dae946bd7e9a064146545/py_spy-0.4.2.tar.gz", hash = "sha256:90e600b27bb6bb40479637baca5a5b4bc2ba3395c93d889e672315d93042c4ae", size = 286374, upload-time = "2026-04-24T22:08:54.906Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/21/ec030145a0c7992bd4b9eafb2f06f56358b3a5339eab4a16534baf3c69aa/py_spy-0.4.2-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:1ccf688393105111684435f035bc14ec3f22117dd2b85b2414612cf27a22755a", size = 3743992, upload-time = "2026-04-24T22:08:45.438Z" }, + { url = "https://files.pythonhosted.org/packages/50/80/de5fd27243c2be03692ecd317bf0dbe24b4c6f78f689ce111e7277a7cb09/py_spy-0.4.2-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:a0e6f6810ccf0fc5e64e85e0182a5b626c4496eec01b14fb8755154b363a4831", size = 1859057, upload-time = "2026-04-24T22:08:46.946Z" }, + { url = "https://files.pythonhosted.org/packages/89/23/3eb4c23c684ebd667674ce1d076ae855e0621d1d9bd5e052aa3f7982f757/py_spy-0.4.2-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:142887e984a4e541071c99a4401ff8c3770f255d329dbd0f64e8c1dd51882cce", size = 2828136, upload-time = "2026-04-24T22:08:48.519Z" }, + { url = "https://files.pythonhosted.org/packages/ca/01/6314152cf9ad3310ebacbf2c47b5ed858086530f8e12b1a665725ca5e0f4/py_spy-0.4.2-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f1c6d9b0e2379ead5bf792df43f4cf36153aa79e6dda4fb8ac7740cf8017110", size = 2857707, upload-time = "2026-04-24T22:08:49.677Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1f/0960a129d504728d28a51dbd5a04ce94031eb75bac676341da7aefdd8232/py_spy-0.4.2-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24720573f95230653b457671a1dcc3c5a381fcf4e92677761e328a430ad251b2", size = 2301852, upload-time = "2026-04-24T22:08:51.152Z" }, + { url = "https://files.pythonhosted.org/packages/f9/34/dd7d3c763a00b7b965e25a5eab0acd1a345dbaf0f45fffe595278873a1c0/py_spy-0.4.2-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:aeb0323409199c785f730645e9f4bb7a7b9ca2c481f2c331a55642b5d13fa52f", size = 2936518, upload-time = "2026-04-24T22:08:52.264Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ed/1409cdb557e558a6c98003ab12fdd4284699e158c167c187cb0f124eea4c/py_spy-0.4.2-py2.py3-none-win_amd64.whl", hash = "sha256:8b06a353c177677e4e1701b288d8c58e2f8d4208ee81a8048d9f72ba800918f8", size = 1894002, upload-time = "2026-04-24T22:08:53.811Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[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 = "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.3" +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/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/98/b50eb9a411e87483b5c65dba4fa430a06bac4234d3403a40e5a9905ebcd0/pydantic_core-2.46.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1", size = 2108971, upload-time = "2026-04-20T14:43:51.945Z" }, + { url = "https://files.pythonhosted.org/packages/08/4b/f364b9d161718ff2217160a4b5d41ce38de60aed91c3689ebffa1c939d23/pydantic_core-2.46.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f", size = 1949588, upload-time = "2026-04-20T14:44:10.386Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8b/30bd03ee83b2f5e29f5ba8e647ab3c456bf56f2ec72fdbcc0215484a0854/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3", size = 1975986, upload-time = "2026-04-20T14:43:57.106Z" }, + { url = "https://files.pythonhosted.org/packages/3c/54/13ccf954d84ec275d5d023d5786e4aa48840bc9f161f2838dc98e1153518/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a", size = 2055830, upload-time = "2026-04-20T14:44:15.499Z" }, + { url = "https://files.pythonhosted.org/packages/be/0e/65f38125e660fdbd72aa858e7dfae893645cfa0e7b13d333e174a367cd23/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807", size = 2222340, upload-time = "2026-04-20T14:41:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/d1/88/f3ab7739efe0e7e80777dbb84c59eb98518e3f57ea433206194c2e425272/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda", size = 2280727, upload-time = "2026-04-20T14:41:30.461Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6d/c228219080817bec4982f9531cadb18da6aaa770fdeb114f49c237ac2c9f/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57", size = 2092158, upload-time = "2026-04-20T14:44:07.305Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b1/525a16711e7c6d61635fac3b0bd54600b5c5d9f60c6fc5aaab26b64a2297/pydantic_core-2.46.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045", size = 2116626, upload-time = "2026-04-20T14:42:34.118Z" }, + { url = "https://files.pythonhosted.org/packages/ef/7c/17d30673351439a6951bf54f564cf2443ab00ae264ec9df00e2efd710eb5/pydantic_core-2.46.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943", size = 2160691, upload-time = "2026-04-20T14:41:14.023Z" }, + { url = "https://files.pythonhosted.org/packages/86/66/af8adbcbc0886ead7f1a116606a534d75a307e71e6e08226000d51b880d2/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f", size = 2182543, upload-time = "2026-04-20T14:40:48.886Z" }, + { url = "https://files.pythonhosted.org/packages/b0/37/6de71e0f54c54a4190010f57deb749e1ddf75c568ada3b1320b70067f121/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4", size = 2324513, upload-time = "2026-04-20T14:42:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/51/b1/9fc74ce94f603d5ef59ff258ca9c2c8fb902fb548d340a96f77f4d1c3b7f/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a", size = 2361853, upload-time = "2026-04-20T14:43:24.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/d0/4c652fc592db35f100279ee751d5a145aca1b9a7984b9684ba7c1b5b0535/pydantic_core-2.46.3-cp310-cp310-win32.whl", hash = "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7", size = 1980465, upload-time = "2026-04-20T14:44:46.239Z" }, + { url = "https://files.pythonhosted.org/packages/27/b8/a920453c38afbe1f355e1ea0b0d94a0a3e0b0879d32d793108755fa171d5/pydantic_core-2.46.3-cp310-cp310-win_amd64.whl", hash = "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6", size = 2073884, upload-time = "2026-04-20T14:43:01.201Z" }, + { url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" }, + { url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" }, + { url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" }, + { url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, + { url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" }, + { url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" }, + { url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" }, +] + +[[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 = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[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-discovery" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, +] + +[[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/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "ray" +version = "2.55.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/7e/d0/a85097dd53aaca1a44acc4dd0b3d2c0e9233179433e2ee326e4018ab3cf7/ray-2.55.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:2d5786661e192148719accc959def6cdcabd7a24cd9008005bf3d0e3c8cfd529", size = 65829601, upload-time = "2026-04-22T20:09:10.013Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/413baab5f0bdd1f913bd46538d96df3547a495b1a0de42f776b5c80d821c/ray-2.55.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:baf2ec89df7838cabdef493ff9bdbec1e6a6452f8bc696ad0c1b8a6198721745", size = 72776751, upload-time = "2026-04-22T20:09:17.802Z" }, + { url = "https://files.pythonhosted.org/packages/b4/64/640f5525bac171282c6f76f3ecc9c4cfef60149ac0d00231afb22018ebe5/ray-2.55.1-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:bb49fbbe53a1d931e1f92d17f9271338f0b738885f8f70b7f531aa33f019d8af", size = 73606971, upload-time = "2026-04-22T20:09:23.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/9a/917f25438d802e23cee2bd1426f1e36ae19e0d0e41908d50937e0a4b7fd4/ray-2.55.1-cp310-cp310-win_amd64.whl", hash = "sha256:86e618e9ad8c6a24331c788eb599cee9838a62d2e10dfca0227743be06cf551c", size = 27886803, upload-time = "2026-04-22T20:09:28.747Z" }, + { url = "https://files.pythonhosted.org/packages/88/7d/48ba2f49b40a34b0071ee27c0144a2573d8836094eaca213d59cef12c271/ray-2.55.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0053fd5b400f7ac56263aa1bbd3d68fb79341b08b8dc697c88782d5aca7b3ed4", size = 65835271, upload-time = "2026-04-22T20:09:34.984Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a3/d6db3a428e4ea17cc72e79f747cfe11e90e63e36e1705bb8324e45f334b7/ray-2.55.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:0ea2f670a7725833ad2333a8c46ab69865ad06c8e5de9f65695e0f8f35331cec", size = 72879783, upload-time = "2026-04-22T20:09:40.986Z" }, + { url = "https://files.pythonhosted.org/packages/46/59/41da0e72a59cd3e8978480ccfeb86ef4235ae5ceb9b8928168a764fa930a/ray-2.55.1-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:d5382da181c03ee2f502ef46cf0ae4bbc30157b5bd9a67d7651f6a272528a85a", size = 73706515, upload-time = "2026-04-22T20:09:47.079Z" }, + { url = "https://files.pythonhosted.org/packages/65/52/c16bbdc3e31a5178f97be88966ab56db6f7e04882640c5cf2fee5b87757b/ray-2.55.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e56d2e8f304cafe990c198a2b894f5b813de018998cd7212869201f6dc17cff", size = 27882093, upload-time = "2026-04-22T20:09:52.943Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3a/4d34f471a68b958b7f94c974c19ad6836a61a2dc16393df4294169a2e4b0/ray-2.55.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:137f9006eee28caab8260803cca314f37bbda3fc94fdfa31c770b5d019626ad8", size = 65822379, upload-time = "2026-04-22T20:09:58.064Z" }, + { url = "https://files.pythonhosted.org/packages/f1/13/0db535102d0256b350ca116d8987588aca1a1f9ebb4638e1e1ff88bbcef8/ray-2.55.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:26541f69bb55607ef8335baac75b2ed12ff2ce02d56313219b29eda003039221", size = 72910802, upload-time = "2026-04-22T20:10:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f8/fffadf3f4285eebd460e4d7f2ed1c0cd641ed89613c3f49eb881ee9fa7e2/ray-2.55.1-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:263705f6bab29e7622a94f82da25fd7f9cead76cdf89a07aab28f79cdf8f9d95", size = 73765203, upload-time = "2026-04-22T20:10:10.495Z" }, + { url = "https://files.pythonhosted.org/packages/10/f7/5acb86fc9625a0e6bbc40e1c7d42c60770e78585439a921c32738b6d675a/ray-2.55.1-cp312-cp312-win_amd64.whl", hash = "sha256:9ad56704c8bd7e92130162f9c58e4ef473609515637673d5a36e761f95335206", size = 27865547, upload-time = "2026-04-22T20:10:15.364Z" }, + { url = "https://files.pythonhosted.org/packages/d5/95/898699cc1a6a5f304ea95376d079843b5c05f4c8c1ec7e55a5cc7ffcea50/ray-2.55.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:f9844a9272ef2e6eb5771025866072cf4234cf4c7cc1a31e235b7de7111864be", size = 65766823, upload-time = "2026-04-22T20:10:20.786Z" }, + { url = "https://files.pythonhosted.org/packages/c9/13/87deecc090c672e45a0cf6f5eef511de448b93f37ef18fd10eb8e8557a0d/ray-2.55.1-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:b415d590e062f248907e0fe42994943f11726b7178fcf4b1cf5546721fb1a5f8", size = 72818676, upload-time = "2026-04-22T20:10:26.705Z" }, + { url = "https://files.pythonhosted.org/packages/71/d7/fc95d3b8824c62105c64aa1b59c59600b581f608d78a2af753e010936dc9/ray-2.55.1-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:1380e043eb57cde69b7e9199c6f2558ceeb8f0fc41c97d1d5e50ea042115f302", size = 73678908, upload-time = "2026-04-22T20:10:32.795Z" }, + { url = "https://files.pythonhosted.org/packages/a9/03/7e552325572e067b23a4584bda8dc6a67af8bd7e03c424d2610bfa93112d/ray-2.55.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:b062045c64c2bce39a51661624f7292c7bbf30f2a9d878627aae31d46da5712d", size = 65774106, upload-time = "2026-04-22T20:10:39.885Z" }, + { url = "https://files.pythonhosted.org/packages/94/62/607a8859520ce350861425f11f8e15d66c15ee33e6aac812f9e2889b5df4/ray-2.55.1-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:4e618d61e1b14b6fde9a586151f3fd9d435b0b85048b997bcaa7f4a533747b2b", size = 72814044, upload-time = "2026-04-22T20:10:46.985Z" }, + { url = "https://files.pythonhosted.org/packages/04/5a/0699bef04a72d7dc54462960d07ef7a19cd8b1e09979880aba2b6d13cca2/ray-2.55.1-cp314-cp314-manylinux2014_x86_64.whl", hash = "sha256:156ed3e72ad95b645d2006cd71a8dddbcc89b56bfc00027f6225adf78bd9cb74", size = 73644244, upload-time = "2026-04-22T20:10:52.973Z" }, +] + +[package.optional-dependencies] +default = [ + { name = "aiohttp" }, + { name = "aiohttp-cors" }, + { name = "colorful" }, + { name = "grpcio" }, + { name = "opencensus" }, + { name = "opentelemetry-exporter-prometheus" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "prometheus-client" }, + { name = "py-spy" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "smart-open" }, + { name = "virtualenv" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +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 = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[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 = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[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 = "smart-open" +version = "7.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/33/7a00ac9b4a63afb4279b99a766f6cbe56c443526dcbf5db97b219e21fde9/smart_open-7.6.0.tar.gz", hash = "sha256:44717f46b5ff276fac03b88e5d13d1c416f064f3b7b081381b0fa8889004bd7e", size = 54548, upload-time = "2026-04-13T09:48:04.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/bc/2761410d0541e975f384bc89f062d716bf119499dd097eb1af33dcd3b1c0/smart_open-7.6.0-py3-none-any.whl", hash = "sha256:2a78f454610a826aa688065b54b4a0a9b12a5599fa61d5190e9bac2df5e5f53f", size = 64591, upload-time = "2026-04-13T09:48:02.687Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[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.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/98/3a7e644e19cb26133488caff231be390579860bbbb3da35913c49a1d0a46/virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada", size = 5850742, upload-time = "2026-04-14T22:15:31.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" }, +] + +[[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 = "wrapt" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/d2/387594fb592d027366645f3d7cc9b4d7ca7be93845fbaba6d835a912ef3c/wrapt-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a86d99a14f76facb269dc148590c01aaf47584071809a70da30555228158c", size = 60669, upload-time = "2026-03-06T02:52:40.671Z" }, + { url = "https://files.pythonhosted.org/packages/c9/18/3f373935bc5509e7ac444c8026a56762e50c1183e7061797437ca96c12ce/wrapt-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a819e39017f95bf7aede768f75915635aa8f671f2993c036991b8d3bfe8dbb6f", size = 61603, upload-time = "2026-03-06T02:54:21.032Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7a/32758ca2853b07a887a4574b74e28843919103194bb47001a304e24af62f/wrapt-2.1.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5681123e60aed0e64c7d44f72bbf8b4ce45f79d81467e2c4c728629f5baf06eb", size = 113632, upload-time = "2026-03-06T02:53:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d5/eeaa38f670d462e97d978b3b0d9ce06d5b91e54bebac6fbed867809216e7/wrapt-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8b28e97a44d21836259739ae76284e180b18abbb4dcfdff07a415cf1016c3e", size = 115644, upload-time = "2026-03-06T02:54:53.33Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/2a41506cb17affb0bdf9d5e2129c8c19e192b388c4c01d05e1b14db23c00/wrapt-2.1.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cef91c95a50596fcdc31397eb6955476f82ae8a3f5a8eabdc13611b60ee380ba", size = 112016, upload-time = "2026-03-06T02:54:43.274Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/0e6c3f5e87caadc43db279724ee36979246d5194fa32fed489c73643ba59/wrapt-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dad63212b168de8569b1c512f4eac4b57f2c6934b30df32d6ee9534a79f1493f", size = 114823, upload-time = "2026-03-06T02:54:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/56/b2/0ad17c8248f4e57bedf44938c26ec3ee194715f812d2dbbd9d7ff4be6c06/wrapt-2.1.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d307aa6888d5efab2c1cde09843d48c843990be13069003184b67d426d145394", size = 111244, upload-time = "2026-03-06T02:54:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/ff/04/bcdba98c26f2c6522c7c09a726d5d9229120163493620205b2f76bd13c01/wrapt-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c87cf3f0c85e27b3ac7d9ad95da166bf8739ca215a8b171e8404a2d739897a45", size = 113307, upload-time = "2026-03-06T02:54:12.428Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1b/5e2883c6bc14143924e465a6fc5a92d09eeabe35310842a481fb0581f832/wrapt-2.1.2-cp310-cp310-win32.whl", hash = "sha256:d1c5fea4f9fe3762e2b905fdd67df51e4be7a73b7674957af2d2ade71a5c075d", size = 57986, upload-time = "2026-03-06T02:54:26.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/5a/4efc997bccadd3af5749c250b49412793bc41e13a83a486b2b54a33e240c/wrapt-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:d8f7740e1af13dff2684e4d56fe604a7e04d6c94e737a60568d8d4238b9a0c71", size = 60336, upload-time = "2026-03-06T02:54:18Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f5/a2bb833e20181b937e87c242645ed5d5aa9c373006b0467bfe1a35c727d0/wrapt-2.1.2-cp310-cp310-win_arm64.whl", hash = "sha256:1c6cc827c00dc839350155f316f1f8b4b0c370f52b6a19e782e2bda89600c7dc", size = 58757, upload-time = "2026-03-06T02:53:51.545Z" }, + { url = "https://files.pythonhosted.org/packages/c7/81/60c4471fce95afa5922ca09b88a25f03c93343f759aae0f31fb4412a85c7/wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb", size = 60666, upload-time = "2026-03-06T02:52:58.934Z" }, + { url = "https://files.pythonhosted.org/packages/6b/be/80e80e39e7cb90b006a0eaf11c73ac3a62bbfb3068469aec15cc0bc795de/wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d", size = 61601, upload-time = "2026-03-06T02:53:00.487Z" }, + { url = "https://files.pythonhosted.org/packages/b0/be/d7c88cd9293c859fc74b232abdc65a229bb953997995d6912fc85af18323/wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894", size = 114057, upload-time = "2026-03-06T02:52:44.08Z" }, + { url = "https://files.pythonhosted.org/packages/ea/25/36c04602831a4d685d45a93b3abea61eca7fe35dab6c842d6f5d570ef94a/wrapt-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c691a6bc752c0cc4711cc0c00896fcd0f116abc253609ef64ef930032821842", size = 116099, upload-time = "2026-03-06T02:54:56.74Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4e/98a6eb417ef551dc277bec1253d5246b25003cf36fdf3913b65cb7657a56/wrapt-2.1.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f3b7d73012ea75aee5844de58c88f44cf62d0d62711e39da5a82824a7c4626a8", size = 112457, upload-time = "2026-03-06T02:53:52.842Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a6/a6f7186a5297cad8ec53fd7578533b28f795fdf5372368c74bd7e6e9841c/wrapt-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577dff354e7acd9d411eaf4bfe76b724c89c89c8fc9b7e127ee28c5f7bcb25b6", size = 115351, upload-time = "2026-03-06T02:53:32.684Z" }, + { url = "https://files.pythonhosted.org/packages/97/6f/06e66189e721dbebd5cf20e138acc4d1150288ce118462f2fcbff92d38db/wrapt-2.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d7b6fd105f8b24e5bd23ccf41cb1d1099796524bcc6f7fbb8fe576c44befbc9", size = 111748, upload-time = "2026-03-06T02:53:08.455Z" }, + { url = "https://files.pythonhosted.org/packages/ef/43/4808b86f499a51370fbdbdfa6cb91e9b9169e762716456471b619fca7a70/wrapt-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:866abdbf4612e0b34764922ef8b1c5668867610a718d3053d59e24a5e5fcfc15", size = 113783, upload-time = "2026-03-06T02:53:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/91/2c/a3f28b8fa7ac2cefa01cfcaca3471f9b0460608d012b693998cd61ef43df/wrapt-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5a0a0a3a882393095573344075189eb2d566e0fd205a2b6414e9997b1b800a8b", size = 57977, upload-time = "2026-03-06T02:53:27.844Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c3/2b1c7bd07a27b1db885a2fab469b707bdd35bddf30a113b4917a7e2139d2/wrapt-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:64a07a71d2730ba56f11d1a4b91f7817dc79bc134c11516b75d1921a7c6fcda1", size = 60336, upload-time = "2026-03-06T02:54:28.104Z" }, + { url = "https://files.pythonhosted.org/packages/ec/5c/76ece7b401b088daa6503d6264dd80f9a727df3e6042802de9a223084ea2/wrapt-2.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:b89f095fe98bc12107f82a9f7d570dc83a0870291aeb6b1d7a7d35575f55d98a", size = 58756, upload-time = "2026-03-06T02:53:16.319Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, + { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, + { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, + { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, + { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, + { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, + { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, + { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, + { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, + { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, + { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, + { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, + { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0d/9cc638702f6fc3c7a3685bcc8cf2a9ed7d6206e932a49f5242658047ef51/yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", size = 123764, upload-time = "2026-03-01T22:04:09.7Z" }, + { url = "https://files.pythonhosted.org/packages/7a/35/5a553687c5793df5429cd1db45909d4f3af7eee90014888c208d086a44f0/yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", size = 86282, upload-time = "2026-03-01T22:04:11.892Z" }, + { url = "https://files.pythonhosted.org/packages/68/2e/c5a2234238f8ce37a8312b52801ee74117f576b1539eec8404a480434acc/yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", size = 86053, upload-time = "2026-03-01T22:04:13.292Z" }, + { url = "https://files.pythonhosted.org/packages/74/3f/bbd8ff36fb038622797ffbaf7db314918bb4d76f1cc8a4f9ca7a55fe5195/yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", size = 99395, upload-time = "2026-03-01T22:04:15.133Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/9516bc4e269d2a3ec9c6779fcdeac51ce5b3a9b0156f06ac7152e5bba864/yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", size = 92143, upload-time = "2026-03-01T22:04:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/c7/63/88802d1f6b1cb1fc67d67a58cd0cf8a1790de4ce7946e434240f1d60ab4a/yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", size = 107643, upload-time = "2026-03-01T22:04:18.519Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/4f9b838f4d8bdd6f0f385aed8bbf21c71ed11a0b9983305c302cbd557815/yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", size = 108700, upload-time = "2026-03-01T22:04:20.373Z" }, + { url = "https://files.pythonhosted.org/packages/50/12/95a1d33f04a79c402664070d43b8b9f72dc18914e135b345b611b0b1f8cc/yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", size = 102769, upload-time = "2026-03-01T22:04:23.055Z" }, + { url = "https://files.pythonhosted.org/packages/86/65/91a0285f51321369fd1a8308aa19207520c5f0587772cfc2e03fc2467e90/yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", size = 101114, upload-time = "2026-03-01T22:04:25.031Z" }, + { url = "https://files.pythonhosted.org/packages/58/80/c7c8244fc3e5bc483dc71a09560f43b619fab29301a0f0a8f936e42865c7/yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", size = 98883, upload-time = "2026-03-01T22:04:27.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/71ca9cc9ca79c0b7d491216177d1aed559d632947b8ffb0ee60f7d8b23e3/yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", size = 94172, upload-time = "2026-03-01T22:04:28.554Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3f/6c6c8a0fe29c26fb2db2e8d32195bb84ec1bfb8f1d32e7f73b787fcf349b/yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", size = 107010, upload-time = "2026-03-01T22:04:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/56/38/12730c05e5ad40a76374d440ed8b0899729a96c250516d91c620a6e38fc2/yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", size = 100285, upload-time = "2026-03-01T22:04:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/34/92/6a7be9239f2347234e027284e7a5f74b1140cc86575e7b469d13fba1ebfe/yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", size = 108230, upload-time = "2026-03-01T22:04:33.844Z" }, + { url = "https://files.pythonhosted.org/packages/5e/81/4aebccfa9376bd98b9d8bfad20621a57d3e8cfc5b8631c1fa5f62cdd03f4/yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", size = 103008, upload-time = "2026-03-01T22:04:35.856Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/0b4e3edcec794a86b853b0c6396c0a888d72dfce19b2d88c02ac289fb6c1/yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", size = 83073, upload-time = "2026-03-01T22:04:38.268Z" }, + { url = "https://files.pythonhosted.org/packages/a0/71/ad95c33da18897e4c636528bbc24a1dd23fe16797de8bc4ec667b8db0ba4/yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", size = 87328, upload-time = "2026-03-01T22:04:39.558Z" }, + { url = "https://files.pythonhosted.org/packages/e2/14/dfa369523c79bccf9c9c746b0a63eb31f65db9418ac01275f7950962e504/yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", size = 82463, upload-time = "2026-03-01T22:04:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, + { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +] diff --git a/skills/launch-nemo-rl/SKILL.md b/skills/launch-nemo-rl/SKILL.md index 3d5caa4c6b9..33e28aba0e4 100644 --- a/skills/launch-nemo-rl/SKILL.md +++ b/skills/launch-nemo-rl/SKILL.md @@ -7,7 +7,7 @@ allowed-tools: Bash Read Grep Glob Edit Write # launch-nemo-rl — running NeMo-RL recipes on Kubernetes via nrl-k8s -This is the playbook for the `nrl-k8s` CLI at `tools/nrl_k8s/`. Follow it when the user asks to launch / iterate / debug a NeMo-RL recipe on a Kubernetes cluster. Verify current state (`kubectl`, `git log`, the recipe + infra files) before acting — the cluster is shared and the cost of a wrong action is high. +This is the playbook for the `nrl-k8s` CLI at `infra/nrl_k8s/`. Follow it when the user asks to launch / iterate / debug a NeMo-RL recipe on a Kubernetes cluster. Verify current state (`kubectl`, `git log`, the recipe + infra files) before acting — the cluster is shared and the cost of a wrong action is high. ## 1. One command, two modes @@ -35,14 +35,14 @@ The rest of the CLI is observability / stage-by-stage control: Every launch takes two files. Pass the infra with `--infra`, not merged inline: ``` -nrl-k8s run tools/nrl_k8s/examples/.yaml \ - --infra tools/nrl_k8s/examples/..infra.yaml +nrl-k8s run infra/nrl_k8s/examples/.yaml \ + --infra infra/nrl_k8s/examples/..infra.yaml ``` - **Recipe** (e.g. `qwen3_30b_math_8n_4gpu.yaml`) — NeMo-RL config: model, GRPO/SFT knobs, `cluster.{gpus_per_node,num_nodes}`. Uses `defaults:` to inherit from `examples/configs/recipes/llm/...`. - **Infra** (e.g. `*..infra.yaml`) — K8s/Ray shape: namespace, image, service account, RayCluster spec, `submit.submitter`, `launch.{mode,codeSource,codePath,entrypoint}`. Pair names follow `.[.prod].infra.yaml` where `` names the hardware target (e.g. `gb300`). -Example pairs in `tools/nrl_k8s/examples/` — read the neighbouring files to see the current conventions for the target profile. +Example pairs in `infra/nrl_k8s/examples/` — read the neighbouring files to see the current conventions for the target profile. ## 3. Long-lived mode flags @@ -88,7 +88,7 @@ entrypoint: | cd /opt/nemo-rl RUN_ID="\${RAY_JOB_SUBMISSION_ID:-\${NRL_K8S_RUN_ID:-$(date -u +%Y%m%d-%H%M%S)}}" python -u examples/run_grpo.py \ - --config tools/nrl_k8s/examples/.yaml \ + --config infra/nrl_k8s/examples/.yaml \ logger.wandb_enabled=true \ logger.wandb.project= \ "logger.wandb.name=-\${RUN_ID}" @@ -98,7 +98,7 @@ entrypoint: | ## 6. Per-profile concerns (hardware + scheduler + DRA) -Every infra YAML encodes a hardware/scheduler profile. The concrete examples in `tools/nrl_k8s/examples/` are authoritative for the profiles they target — read the neighbouring infra file before writing a new one. Things that commonly vary: +Every infra YAML encodes a hardware/scheduler profile. The concrete examples in `infra/nrl_k8s/examples/` are authoritative for the profiles they target — read the neighbouring infra file before writing a new one. Things that commonly vary: - **Per-node GPUs** (e.g. 4 vs 8) — must match `cluster.gpus_per_node` in the recipe, otherwise workers stay `Pending`. - **Node selectors** — head pods usually land on a CPU-only node pool; GPU workers match on `nvidia.com/gpu.product` or a node-group label. @@ -252,7 +252,7 @@ The pod's `default` service account needs an `edit` RoleBinding in the namespace ## 14. Where things live in the repo -- CLI code: `tools/nrl_k8s/src/nrl_k8s/` (`cli.py`, `orchestrate.py`, `manifest.py`, `rayjob.py`, `k8s.py`, `submitters/`, `schema.py`). -- Tests: `tools/nrl_k8s/tests/unit/` — run with `uv run --extra test pytest -x -q` from `tools/nrl_k8s/`. -- Recipe + infra examples: `tools/nrl_k8s/examples/`. +- CLI code: `infra/nrl_k8s/src/nrl_k8s/` (`cli.py`, `orchestrate.py`, `manifest.py`, `rayjob.py`, `k8s.py`, `submitters/`, `schema.py`). +- Tests: `infra/nrl_k8s/tests/unit/` — run with `uv run --extra test pytest -x -q` from `infra/nrl_k8s/`. +- Recipe + infra examples: `infra/nrl_k8s/examples/`. - Base recipes this tool wraps: `examples/configs/recipes/llm/…` and `examples/nemo_gym/…`. From d2ef70feafe68c9abe3c6e1564bc829698da5112 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 26 Apr 2026 11:54:22 -0700 Subject: [PATCH 59/84] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94?= =?UTF-8?q?=20copyright=20headers,=20ssh-key=20single,=20workdir=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Terry Kong --- infra/nrl_k8s/src/nrl_k8s/__init__.py | 13 +++++ infra/nrl_k8s/src/nrl_k8s/_logging.py | 13 +++++ infra/nrl_k8s/src/nrl_k8s/_retry.py | 13 +++++ infra/nrl_k8s/src/nrl_k8s/cli.py | 22 ++++++-- infra/nrl_k8s/src/nrl_k8s/config.py | 13 +++++ infra/nrl_k8s/src/nrl_k8s/dev.py | 13 +++++ infra/nrl_k8s/src/nrl_k8s/inspect.py | 13 +++++ infra/nrl_k8s/src/nrl_k8s/k8s.py | 13 +++++ infra/nrl_k8s/src/nrl_k8s/manifest.py | 13 +++++ infra/nrl_k8s/src/nrl_k8s/orchestrate.py | 51 +++++++++++++------ infra/nrl_k8s/src/nrl_k8s/rayjob.py | 13 +++++ infra/nrl_k8s/src/nrl_k8s/schema.py | 13 +++++ infra/nrl_k8s/src/nrl_k8s/submit.py | 13 +++++ .../src/nrl_k8s/submitters/__init__.py | 6 +++ infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py | 6 +++ .../src/nrl_k8s/submitters/portforward.py | 6 +++ infra/nrl_k8s/src/nrl_k8s/workdir.py | 13 +++++ 17 files changed, 227 insertions(+), 20 deletions(-) diff --git a/infra/nrl_k8s/src/nrl_k8s/__init__.py b/infra/nrl_k8s/src/nrl_k8s/__init__.py index 2571088acc0..9c6f586f277 100644 --- a/infra/nrl_k8s/src/nrl_k8s/__init__.py +++ b/infra/nrl_k8s/src/nrl_k8s/__init__.py @@ -1,3 +1,16 @@ +# 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. """nrl-k8s: config-driven launcher for NeMo-RL recipes on Kubernetes.""" __version__ = "0.1.0" diff --git a/infra/nrl_k8s/src/nrl_k8s/_logging.py b/infra/nrl_k8s/src/nrl_k8s/_logging.py index 6a94e99492a..eb2cfc82ca5 100644 --- a/infra/nrl_k8s/src/nrl_k8s/_logging.py +++ b/infra/nrl_k8s/src/nrl_k8s/_logging.py @@ -1,3 +1,16 @@ +# 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. """Redaction helpers used before logging k8s objects. Manifests carry secret material in two common shapes: ConfigMap/Secret diff --git a/infra/nrl_k8s/src/nrl_k8s/_retry.py b/infra/nrl_k8s/src/nrl_k8s/_retry.py index 2364b4b819f..ea4881fb268 100644 --- a/infra/nrl_k8s/src/nrl_k8s/_retry.py +++ b/infra/nrl_k8s/src/nrl_k8s/_retry.py @@ -1,3 +1,16 @@ +# 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. """Retry wrapper for transient Kubernetes API failures. 5xx responses and connection resets are common on busy clusters. Our diff --git a/infra/nrl_k8s/src/nrl_k8s/cli.py b/infra/nrl_k8s/src/nrl_k8s/cli.py index eafba6ad512..58e9dd467b8 100644 --- a/infra/nrl_k8s/src/nrl_k8s/cli.py +++ b/infra/nrl_k8s/src/nrl_k8s/cli.py @@ -1,3 +1,16 @@ +# 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. """``nrl-k8s`` command-line entry point. Hydra-style overrides (``infra.scheduler.queue=x``) are collected via @@ -1260,8 +1273,7 @@ def dev_stop(namespace: str | None) -> None: @click.option( "--ssh-key", type=click.Path(exists=True), - multiple=True, - help="Path to an SSH private key (repeatable).", + help="Path to an SSH private key.", ) @click.option( "--add-rclone", @@ -1271,7 +1283,7 @@ def dev_stop(namespace: str | None) -> None: @click.option("--namespace", "-n", default=None, help="Kubernetes namespace.") def dev_setup_secrets( kvs: tuple[str, ...], - ssh_key: tuple[str, ...], + ssh_key: str | None, add_rclone: bool, namespace: str | None, ) -> None: @@ -1305,8 +1317,8 @@ def dev_setup_secrets( name, val = kv.split("=", 1) data[name] = val - for key_path in ssh_key: - p = Path(key_path) + if ssh_key: + p = Path(ssh_key) data["SSH_KEY_NAME"] = p.name data["SSH_KEY_CONTENT"] = p.read_text() diff --git a/infra/nrl_k8s/src/nrl_k8s/config.py b/infra/nrl_k8s/src/nrl_k8s/config.py index 9a6b998596d..10a14f9fc80 100644 --- a/infra/nrl_k8s/src/nrl_k8s/config.py +++ b/infra/nrl_k8s/src/nrl_k8s/config.py @@ -1,3 +1,16 @@ +# 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. """Recipe + infra config loader for ``nrl-k8s``. The CLI's single source of truth for a run. Given a recipe path, loads and diff --git a/infra/nrl_k8s/src/nrl_k8s/dev.py b/infra/nrl_k8s/src/nrl_k8s/dev.py index 5309846ec9b..530c9ef8f22 100644 --- a/infra/nrl_k8s/src/nrl_k8s/dev.py +++ b/infra/nrl_k8s/src/nrl_k8s/dev.py @@ -1,3 +1,16 @@ +# 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. """Dev pod manifest builder for ``nrl-k8s dev``.""" from __future__ import annotations diff --git a/infra/nrl_k8s/src/nrl_k8s/inspect.py b/infra/nrl_k8s/src/nrl_k8s/inspect.py index 17846ca045d..72f650d0ffb 100644 --- a/infra/nrl_k8s/src/nrl_k8s/inspect.py +++ b/infra/nrl_k8s/src/nrl_k8s/inspect.py @@ -1,3 +1,16 @@ +# 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. """Read-only introspection of the clusters a recipe owns. Used by ``nrl-k8s status`` and ``nrl-k8s logs`` to summarise what's running diff --git a/infra/nrl_k8s/src/nrl_k8s/k8s.py b/infra/nrl_k8s/src/nrl_k8s/k8s.py index a2fa66f4c98..3e2d3468bb4 100644 --- a/infra/nrl_k8s/src/nrl_k8s/k8s.py +++ b/infra/nrl_k8s/src/nrl_k8s/k8s.py @@ -1,3 +1,16 @@ +# 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. """Thin wrapper around the official ``kubernetes`` Python client. RayCluster + RayJob are Kubernetes ``CustomObjectsApi`` resources, so we use diff --git a/infra/nrl_k8s/src/nrl_k8s/manifest.py b/infra/nrl_k8s/src/nrl_k8s/manifest.py index 739bd92396e..46dc247061a 100644 --- a/infra/nrl_k8s/src/nrl_k8s/manifest.py +++ b/infra/nrl_k8s/src/nrl_k8s/manifest.py @@ -1,3 +1,16 @@ +# 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. """Build a RayCluster manifest dict from the recipe's inline ``spec``. The recipe encodes the full RayCluster shape inline under diff --git a/infra/nrl_k8s/src/nrl_k8s/orchestrate.py b/infra/nrl_k8s/src/nrl_k8s/orchestrate.py index 8d88eb96607..1b76c1ec6fd 100644 --- a/infra/nrl_k8s/src/nrl_k8s/orchestrate.py +++ b/infra/nrl_k8s/src/nrl_k8s/orchestrate.py @@ -1,3 +1,16 @@ +# 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. """One-shot orchestration for a disaggregated run. ``nrl-k8s run `` delegates here. The flow: @@ -20,6 +33,7 @@ from __future__ import annotations import re +import shutil import time import urllib.error import urllib.request @@ -296,13 +310,16 @@ def submit_daemon( wd = workdir.stage_workdir(repo_root, include_paths=upload_paths) log(f"[{role}] submitting daemon via {dash}") - job_id = submit.submit_ray_job( - dash, - entrypoint=daemon.entrypoint, - working_dir=wd, - env_vars=daemon.env, - submission_id=submission_id, - ) + try: + job_id = submit.submit_ray_job( + dash, + entrypoint=daemon.entrypoint, + working_dir=wd, + env_vars=daemon.env, + submission_id=submission_id, + ) + finally: + shutil.rmtree(wd, ignore_errors=True) log(f"[{role}] daemon submitted as job {job_id}") if daemon.healthCheckUrl: _wait_for_http(daemon.healthCheckUrl, daemon.healthCheckTimeoutS, log, role) @@ -403,14 +420,18 @@ def submit_training( if run_id: env_vars.setdefault("NRL_K8S_RUN_ID", run_id) - handle = submitter.submit( - name, - infra.namespace, - entrypoint=launch.entrypoint, - run_id=run_id or "", - env_vars=env_vars, - working_dir=wd, - ) + try: + handle = submitter.submit( + name, + infra.namespace, + entrypoint=launch.entrypoint, + run_id=run_id or "", + env_vars=env_vars, + working_dir=wd, + ) + finally: + if wd is not None: + shutil.rmtree(wd, ignore_errors=True) save_handle(handle) log(f"[training] training run handle: kind={handle.kind} id={handle.run_id}") return RunResult( diff --git a/infra/nrl_k8s/src/nrl_k8s/rayjob.py b/infra/nrl_k8s/src/nrl_k8s/rayjob.py index ccff904f90a..9e4b6582a9c 100644 --- a/infra/nrl_k8s/src/nrl_k8s/rayjob.py +++ b/infra/nrl_k8s/src/nrl_k8s/rayjob.py @@ -1,3 +1,16 @@ +# 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. """Build and apply KubeRay ``RayJob`` objects. A RayJob is a RayCluster + Ray Job rolled into one K8s resource: KubeRay diff --git a/infra/nrl_k8s/src/nrl_k8s/schema.py b/infra/nrl_k8s/src/nrl_k8s/schema.py index 4862206d6b9..8236d434e93 100644 --- a/infra/nrl_k8s/src/nrl_k8s/schema.py +++ b/infra/nrl_k8s/src/nrl_k8s/schema.py @@ -1,3 +1,16 @@ +# 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. """Pydantic schema for the ``infra:`` section of a NeMo-RL recipe. A recipe YAML is a standard NeMo-RL recipe plus an optional top-level ``infra:`` diff --git a/infra/nrl_k8s/src/nrl_k8s/submit.py b/infra/nrl_k8s/src/nrl_k8s/submit.py index da0a58ad597..33dedcaa53d 100644 --- a/infra/nrl_k8s/src/nrl_k8s/submit.py +++ b/infra/nrl_k8s/src/nrl_k8s/submit.py @@ -1,3 +1,16 @@ +# 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. """Submit a Ray Job to a named RayCluster. Two dashboard access modes: diff --git a/infra/nrl_k8s/src/nrl_k8s/submitters/__init__.py b/infra/nrl_k8s/src/nrl_k8s/submitters/__init__.py index d09e5f1e944..7b5c48a7ec2 100644 --- a/infra/nrl_k8s/src/nrl_k8s/submitters/__init__.py +++ b/infra/nrl_k8s/src/nrl_k8s/submitters/__init__.py @@ -5,6 +5,12 @@ # 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. """Training-job submitters. Two transports, same shape. The abstraction keeps the orchestrator and CLI diff --git a/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py b/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py index e2378f46059..cfd9e499fe0 100644 --- a/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py +++ b/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py @@ -5,6 +5,12 @@ # 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. """``kubectl exec`` submitter — runs the training entrypoint as a raw backgrounded process on the training head pod. diff --git a/infra/nrl_k8s/src/nrl_k8s/submitters/portforward.py b/infra/nrl_k8s/src/nrl_k8s/submitters/portforward.py index e9a1dbc86b5..9091a315213 100644 --- a/infra/nrl_k8s/src/nrl_k8s/submitters/portforward.py +++ b/infra/nrl_k8s/src/nrl_k8s/submitters/portforward.py @@ -5,6 +5,12 @@ # 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. """Port-forward + Ray Job SDK submitter. Dev-iteration default. Opens ``kubectl port-forward svc/-svc :8265`` diff --git a/infra/nrl_k8s/src/nrl_k8s/workdir.py b/infra/nrl_k8s/src/nrl_k8s/workdir.py index 08dca5bc52f..fb35eb9216e 100644 --- a/infra/nrl_k8s/src/nrl_k8s/workdir.py +++ b/infra/nrl_k8s/src/nrl_k8s/workdir.py @@ -1,3 +1,16 @@ +# 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. """Stage a working directory for Ray Job ``runtime_env.working_dir`` upload. Ray's client-side packager honours ``.gitignore``, which silently drops From d62571d4d0fd96d64917a024b82fd11f704ef052 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 26 Apr 2026 12:42:27 -0700 Subject: [PATCH 60/84] refactor: delete wrapper recipe, use upstream with Hydra overrides, add user secret envFrom Signed-off-by: Terry Kong --- .../qwen3_30b_math_4n_4gpu.gb300.infra.yaml | 39 +++++++++++-------- .../examples/qwen3_30b_math_4n_4gpu.yaml | 25 ------------ 2 files changed, 23 insertions(+), 41 deletions(-) delete mode 100644 infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml diff --git a/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml b/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml index fd713eea986..9e47ffd1628 100644 --- a/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml +++ b/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml @@ -1,4 +1,4 @@ -# Prod-mode infra for qwen3_30b_math_4n_4gpu.yaml on a GB300 NVL72 cluster. +# Prod-mode infra for grpo-qwen3-30ba3b-4n4g-async-1off on a GB300 NVL72 cluster. # # Topology: 4 workers × 4 GPUs = 16 GPUs in a single RayCluster, scheduled by # KAI as one gang. NVLink/MNNVL spans all 16 GPUs via a ComputeDomain channel, @@ -18,37 +18,37 @@ # - rl-workspace PVC bound (shared FSx Lustre, 2.4 TiB, RWX). # - /mnt/rl-workspace/${user:}/nemo-rl contains a git checkout of # https://github.com/NVIDIA-NeMo/RL on branch main. -# - nvcr-secret, wandb-api-key, nemo-rl-endpoint-registry all present. +# - nvcr-secret, ${user:}-secrets (via nrl-k8s dev setup-secrets), +# nemo-rl-endpoint-registry SA all present. # - KAI topology `gb300-topology` is registered and advertises # nvidia.com/gpu.clique as a placement key. # # Usage: -# # Long-lived cluster — idempotent; reuses the live cluster if it -# # already matches, applies if absent. Submits training per invocation. -# nrl-k8s run infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml \ -# --infra infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml \ -# --run-id qwen3-30b-math-$(date +%Y%m%d-%H%M%S) +# RECIPE=examples/configs/recipes/llm/performance/grpo-qwen3-30ba3b-4n4g-async-1off.yaml +# INFRA=infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml # -# # One-shot with auto-teardown (ephemeral RayCluster, no cleanup needed): -# nrl-k8s rayjob infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml \ -# --infra infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml +# # One-shot with auto-teardown (default): +# nrl-k8s run $RECIPE --infra $INFRA --wait +# +# # Long-lived cluster for iterating: +# nrl-k8s cluster up $RECIPE --infra $INFRA --role training --wait +# nrl-k8s run $RECIPE --infra $INFRA --raycluster --wait _shared: headNodeSelector: &head_node_selector nodeGroup: customer-cpu workerNodeSelector: &worker_node_selector nvidia.com/gpu.product: NVIDIA-GB300 + userSecretEnvFrom: &user_secret_env_from + - secretRef: + name: ${user:}-secrets + optional: true headEnv: &shared_head_env - {name: HF_HOME, value: "/mnt/rl-workspace/${user:}/hf-cache"} - {name: HF_HUB_OFFLINE, value: "1"} - {name: TRANSFORMERS_OFFLINE, value: "1"} - {name: NCCL_DEBUG, value: WARN} - {name: RAY_memory_monitor_refresh_ms, value: "0"} - - name: WANDB_API_KEY - valueFrom: - secretKeyRef: - name: wandb-api-key - key: WANDB_API_KEY workerEnv: &shared_worker_env - {name: HF_HOME, value: "/mnt/rl-workspace/${user:}/hf-cache"} - {name: HF_HUB_OFFLINE, value: "1"} @@ -135,8 +135,13 @@ launch: mkdir -p "\${LOG_DIR}" python -u examples/run_grpo.py \ - --config infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml \ + --config examples/configs/recipes/llm/performance/grpo-qwen3-30ba3b-4n4g-async-1off.yaml \ + grpo.max_num_steps=500 \ + grpo.val_period=50 \ + checkpointing.save_period=50 \ logger.wandb_enabled=true \ + logger.tensorboard_enabled=true \ + logger.monitor_gpus=true \ logger.wandb.project=nemorl-single-k8s \ logger.wandb.name=qwen3-30b-math-gb300 \ 2>&1 | tee "\${LOG}" @@ -164,6 +169,7 @@ clusters: containers: - name: ray-head env: *shared_head_env + envFrom: *user_secret_env_from resources: *head_resources ports: *gpu_head_ports volumeMounts: *code_mounts @@ -198,6 +204,7 @@ clusters: containers: - name: ray-worker env: *shared_worker_env + envFrom: *user_secret_env_from resources: *gpu_worker_resources securityContext: capabilities: diff --git a/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml b/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml deleted file mode 100644 index c01b6f59d14..00000000000 --- a/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Qwen3-30B-A3B GRPO on math — **single RayCluster, 4 nodes × 4 GPUs (GB300)**. -# -# Extends the performance async-1off recipe (training + generation inside one -# Ray cluster, non-colocated split: 2 nodes training, 2 nodes generation). -# The 16 workers are a single pod-group on GB300 so NVLink spans all 16 GPUs -# via a ComputeDomain channel claim (see the paired infra file). -# -# Pair with qwen3_30b_math_4n_4gpu.gb300.infra.yaml. -defaults: ../../../examples/configs/recipes/llm/performance/grpo-qwen3-30ba3b-4n4g-async-1off.yaml - -cluster: - gpus_per_node: 4 - num_nodes: 4 -grpo: - max_num_steps: 500 - val_period: 50 -checkpointing: - save_period: 50 -logger: - wandb_enabled: true - wandb: - project: nemorl-single-k8s - name: qwen3-30b-math-4n-4gpu-gb300 - tensorboard_enabled: true - monitor_gpus: true From 9b2ee3cc223e2ff2f484ea443dccec02de184d53 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 26 Apr 2026 13:15:12 -0700 Subject: [PATCH 61/84] chore: pin image to nvcr.io/nvidian/nemo-rl:664d29c-49528955 Signed-off-by: Terry Kong --- infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml b/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml index 9e47ffd1628..ac58b03630a 100644 --- a/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml +++ b/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml @@ -95,7 +95,7 @@ _shared: persistentVolumeClaim: {claimName: rl-workspace} # namespace auto-inferred from kube context (default). -image: nvcr.io/nvidian/nemo-rl:nightly +image: nvcr.io/nvidian/nemo-rl:664d29c-49528955 imagePullSecrets: [nvcr-secret] serviceAccount: nemo-rl-endpoint-registry From 6aa8793ef348cd6d5a54495f1818344656f117ac Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 26 Apr 2026 13:48:04 -0700 Subject: [PATCH 62/84] fix: better job logs errors, copy-pasteable log cmd, comment out offline env vars Signed-off-by: Terry Kong --- .../qwen3_30b_math_4n_4gpu.gb300.infra.yaml | 10 +++--- infra/nrl_k8s/src/nrl_k8s/cli.py | 35 ++++++++++++++++++- infra/nrl_k8s/src/nrl_k8s/k8s.py | 10 ++++++ infra/nrl_k8s/src/nrl_k8s/submit.py | 16 +++++---- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml b/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml index ac58b03630a..6f8a741b74f 100644 --- a/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml +++ b/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml @@ -45,14 +45,16 @@ _shared: optional: true headEnv: &shared_head_env - {name: HF_HOME, value: "/mnt/rl-workspace/${user:}/hf-cache"} - - {name: HF_HUB_OFFLINE, value: "1"} - - {name: TRANSFORMERS_OFFLINE, value: "1"} + # Uncomment to force offline mode once all HF assets are pre-cached. + # Without the cached assets, these will cause download errors. + # - {name: HF_HUB_OFFLINE, value: "1"} + # - {name: TRANSFORMERS_OFFLINE, value: "1"} - {name: NCCL_DEBUG, value: WARN} - {name: RAY_memory_monitor_refresh_ms, value: "0"} workerEnv: &shared_worker_env - {name: HF_HOME, value: "/mnt/rl-workspace/${user:}/hf-cache"} - - {name: HF_HUB_OFFLINE, value: "1"} - - {name: TRANSFORMERS_OFFLINE, value: "1"} + # - {name: HF_HUB_OFFLINE, value: "1"} + # - {name: TRANSFORMERS_OFFLINE, value: "1"} - {name: NCCL_DEBUG, value: WARN} - {name: NCCL_MNNVL_ENABLE, value: "1"} - {name: RAY_memory_monitor_refresh_ms, value: "0"} diff --git a/infra/nrl_k8s/src/nrl_k8s/cli.py b/infra/nrl_k8s/src/nrl_k8s/cli.py index 58e9dd467b8..008d562430f 100644 --- a/infra/nrl_k8s/src/nrl_k8s/cli.py +++ b/infra/nrl_k8s/src/nrl_k8s/cli.py @@ -589,9 +589,10 @@ def _run_rayjob( except Exception as exc: # noqa: BLE001 _explain_and_exit(exc, context=f"rayjob {job_name} apply failed") + job_id_cmd = f"$(kubectl get rayjob {job_name} -n {namespace} -o jsonpath='{{.status.jobId}}')" click.echo( f"follow: kubectl get rayjob {job_name} -n {namespace} -w\n" - f"logs: nrl-k8s job logs {recipe} --role training -f", + f"logs: nrl-k8s job logs {job_id_cmd} {recipe} --infra --role training -f", ) # Default is wait unless user passed --no-wait. if cli_wait is False: @@ -1542,6 +1543,7 @@ def _tail(dashboard: str, job_id: str) -> None: def _tail_daemon(cluster_name: str, namespace: str, submission_id: str) -> None: """Open a dashboard port-forward and tail a Ray Job by submission_id.""" + from . import inspect as ins from . import submit as submit_mod try: @@ -1551,9 +1553,40 @@ def _tail_daemon(cluster_name: str, namespace: str, submission_id: str) -> None: except KeyboardInterrupt: click.echo("\n(interrupted — job continues running)", err=True) except Exception as exc: # noqa: BLE001 + hint = _diagnose_port_forward_failure(cluster_name, namespace) + if hint: + _cli_error(f"tailing {cluster_name} failed: {exc}", hint=hint) _explain_and_exit(exc, context=f"tailing {submission_id} failed") +def _diagnose_port_forward_failure(cluster_name: str, namespace: str) -> str | None: + """Check head pod state to produce a more helpful error message.""" + from . import k8s + + try: + head_pod = f"{cluster_name}-head" + pods = k8s.list_pods_by_label( + f"ray.io/cluster={cluster_name},ray.io/node-type=head", namespace + ) + if not pods: + return ( + f"no head pod found for {cluster_name} — " + f"the cluster may still be provisioning. " + f"check: kubectl get pods -l ray.io/cluster={cluster_name} -n {namespace}" + ) + pod = pods[0] + phase = pod.status.phase if pod.status else "Unknown" + if phase != "Running": + return ( + f"head pod is {phase}, not Running yet — " + f"wait for the cluster to be ready and retry. " + f"check: kubectl get pods -l ray.io/cluster={cluster_name} -n {namespace}" + ) + except Exception: + pass + return None + + def _emit_handle(handle) -> None: # type: ignore[no-untyped-def] """Print the resolved handle + next-step commands to stdout. diff --git a/infra/nrl_k8s/src/nrl_k8s/k8s.py b/infra/nrl_k8s/src/nrl_k8s/k8s.py index 3e2d3468bb4..caca8924d7a 100644 --- a/infra/nrl_k8s/src/nrl_k8s/k8s.py +++ b/infra/nrl_k8s/src/nrl_k8s/k8s.py @@ -465,6 +465,16 @@ def delete_pod(name: str, namespace: str, *, ignore_missing: bool = True) -> Non raise +def list_pods_by_label(label_selector: str, namespace: str) -> list: + """Return pods matching a label selector.""" + load_kubeconfig() + core = client.CoreV1Api() + result = with_retries( + lambda: core.list_namespaced_pod(namespace=namespace, label_selector=label_selector) + ) + return result.items or [] + + def get_pod_phase(name: str, namespace: str) -> str | None: """Return the pod phase (Pending/Running/Succeeded/Failed) or None if not found.""" load_kubeconfig() diff --git a/infra/nrl_k8s/src/nrl_k8s/submit.py b/infra/nrl_k8s/src/nrl_k8s/submit.py index 33dedcaa53d..5d8716abcff 100644 --- a/infra/nrl_k8s/src/nrl_k8s/submit.py +++ b/infra/nrl_k8s/src/nrl_k8s/submit.py @@ -312,12 +312,16 @@ def _wait_for_tcp( deadline = time.monotonic() + timeout_s while time.monotonic() < deadline: if proc.poll() is not None: - # Process is gone — the drain thread may have already consumed - # stdout, so we can only surface the bare exit code here. - raise RuntimeError( - f"kubectl port-forward exited early (rc={proc.returncode}); " - "is the RayCluster head service reachable?" - ) + stderr = "" + if proc.stderr: + try: + stderr = proc.stderr.read().decode(errors="replace").strip() + except Exception: + pass + msg = f"kubectl port-forward exited early (rc={proc.returncode})" + if stderr: + msg += f": {stderr}" + raise RuntimeError(msg) try: with socket.create_connection((host, port), timeout=1): return From 0444edb91a1d028e53ba621e5c601a9d70c598ea Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 26 Apr 2026 14:32:26 -0700 Subject: [PATCH 63/84] fix: RBAC hint uses aggregated edit-with-ray ClusterRole, copy-pasteable heredoc Signed-off-by: Terry Kong --- infra/nrl_k8s/src/nrl_k8s/cli.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/infra/nrl_k8s/src/nrl_k8s/cli.py b/infra/nrl_k8s/src/nrl_k8s/cli.py index 008d562430f..9ccb7495d96 100644 --- a/infra/nrl_k8s/src/nrl_k8s/cli.py +++ b/infra/nrl_k8s/src/nrl_k8s/cli.py @@ -1446,7 +1446,29 @@ def _check_dev_pod_rbac(namespace: str) -> None: ) if result.stdout.strip() == "yes": return - rolebinding = ( + heredoc = ( + f"kubectl apply -f - <<'EOF'\n" + f"apiVersion: rbac.authorization.k8s.io/v1\n" + f"kind: ClusterRole\n" + f"metadata:\n" + f" name: edit-with-ray\n" + f"aggregationRule:\n" + f" clusterRoleSelectors:\n" + f" - matchLabels:\n" + f" rbac.authorization.k8s.io/aggregate-to-edit: \"true\"\n" + f"rules: [] # auto-filled by aggregation\n" + f"---\n" + f"apiVersion: rbac.authorization.k8s.io/v1\n" + f"kind: ClusterRole\n" + f"metadata:\n" + f" name: ray-edit\n" + f" labels:\n" + f" rbac.authorization.k8s.io/aggregate-to-edit: \"true\"\n" + f"rules:\n" + f" - apiGroups: [ray.io]\n" + f" resources: [rayjobs, rayclusters]\n" + f" verbs: [get, list, watch, create, update, patch, delete]\n" + f"---\n" f"apiVersion: rbac.authorization.k8s.io/v1\n" f"kind: RoleBinding\n" f"metadata:\n" @@ -1458,13 +1480,14 @@ def _check_dev_pod_rbac(namespace: str) -> None: f" namespace: {namespace}\n" f"roleRef:\n" f" kind: ClusterRole\n" - f" name: edit\n" - f" apiGroup: rbac.authorization.k8s.io" + f" name: edit-with-ray\n" + f" apiGroup: rbac.authorization.k8s.io\n" + f"EOF" ) _cli_error( f"the default service account in {namespace} lacks edit permissions — " f"kubectl won't work inside the dev pod", - hint=f"apply this RoleBinding, then retry:\n\n{rolebinding}", + hint=f"run this, then retry:\n\n{heredoc}", ) From 91feff50d1f13b68a0d21078fb682f6f1693e8c8 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 26 Apr 2026 15:21:33 -0700 Subject: [PATCH 64/84] fix: detect head-svc name collision between rayjob and raycluster Signed-off-by: Terry Kong --- infra/nrl_k8s/src/nrl_k8s/cli.py | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/infra/nrl_k8s/src/nrl_k8s/cli.py b/infra/nrl_k8s/src/nrl_k8s/cli.py index 9ccb7495d96..f9ff9e90085 100644 --- a/infra/nrl_k8s/src/nrl_k8s/cli.py +++ b/infra/nrl_k8s/src/nrl_k8s/cli.py @@ -523,6 +523,12 @@ def run( err=True, ) + namespace = loaded.infra.namespace + for _role in ALL_ROLES: + _cl = getattr(loaded.infra.clusters, _role, None) + if _cl is not None: + _check_head_svc_collision(_cl.name, namespace, creating="raycluster") + try: result = orchestrate.run( loaded, @@ -579,6 +585,7 @@ def _run_rayjob( _preflight_or_exit(namespace) _check_stale_rayjobs(loaded, namespace) + _check_head_svc_collision(job_name, namespace, creating="rayjob") orchestrate.ensure_dra_resources("training", loaded, log=click.echo) click.echo(f"[run --rayjob] applying RayJob {job_name} in {namespace}") @@ -782,6 +789,8 @@ def cluster_up( click.echo(yaml.safe_dump(manifest, sort_keys=False).rstrip()) return + _check_head_svc_collision(cluster_spec.name, loaded.infra.namespace, creating="raycluster") + try: name = orchestrate.bring_up_cluster( role, loaded, log=click.echo, wait_ready=wait, ready_timeout_s=timeout @@ -1403,6 +1412,41 @@ def _error_on_stale(stale: list[_StaleResource], namespace: str) -> None: ) +def _check_head_svc_collision( + name: str, + namespace: str, + *, + creating: str, +) -> None: + """Fail if creating this resource would collide with an existing one's head-svc. + + KubeRay derives the head Service name as ``{name}-head-svc`` for both + RayJobs and RayClusters. When both exist with the same metadata name the + second resource can never create its Service and silently hangs. + """ + from . import k8s + + if creating == "rayjob": + existing = k8s.get_raycluster(name, namespace) + other_kind = "raycluster" + else: + existing = k8s.get_rayjob(name, namespace) + other_kind = "rayjob" + + if existing is None: + return + + _cli_error( + f"a {other_kind} named '{name}' already exists in namespace {namespace}. " + f"KubeRay names the head Service '{name}-head-svc' for both resource types; " + f"creating this {creating} with the same name will collide on the Service " + f"and the new resource will hang indefinitely.", + hint=f"either delete the existing resource:\n" + f" kubectl delete {other_kind} {name} -n {namespace}\n" + f"or use a different name for the {creating}", + ) + + def _check_stale_rayjobs(loaded: LoadedConfig, namespace: str) -> None: """Check all roles for existing RayJobs upfront. From df77ae7af20c0545c19200db312e135fcef5c74a Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 26 Apr 2026 15:26:29 -0700 Subject: [PATCH 65/84] chore: reduce qwen3-30b example to 5 steps for quick testing Signed-off-by: Terry Kong --- .../examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml b/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml index 6f8a741b74f..28c531bd4e4 100644 --- a/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml +++ b/infra/nrl_k8s/examples/qwen3_30b_math_4n_4gpu.gb300.infra.yaml @@ -138,9 +138,9 @@ launch: python -u examples/run_grpo.py \ --config examples/configs/recipes/llm/performance/grpo-qwen3-30ba3b-4n4g-async-1off.yaml \ - grpo.max_num_steps=500 \ - grpo.val_period=50 \ - checkpointing.save_period=50 \ + grpo.max_num_steps=5 \ + grpo.val_period=5 \ + checkpointing.save_period=5 \ logger.wandb_enabled=true \ logger.tensorboard_enabled=true \ logger.monitor_gpus=true \ From c2a3c15397fc96fe66c35a4d0e2af6e280a11847 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 26 Apr 2026 15:42:21 -0700 Subject: [PATCH 66/84] fix: add DRA resource permissions to dev pod RBAC heredoc The ray-edit ClusterRole only covered ray.io CRDs. Running nrl-k8s from the dev pod also needs create/delete on computedomains (resource.nvidia.com) and resourceclaimtemplates (resource.k8s.io). Signed-off-by: Terry Kong --- infra/nrl_k8s/src/nrl_k8s/cli.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infra/nrl_k8s/src/nrl_k8s/cli.py b/infra/nrl_k8s/src/nrl_k8s/cli.py index f9ff9e90085..462319bdbdc 100644 --- a/infra/nrl_k8s/src/nrl_k8s/cli.py +++ b/infra/nrl_k8s/src/nrl_k8s/cli.py @@ -1512,6 +1512,12 @@ def _check_dev_pod_rbac(namespace: str) -> None: f" - apiGroups: [ray.io]\n" f" resources: [rayjobs, rayclusters]\n" f" verbs: [get, list, watch, create, update, patch, delete]\n" + f" - apiGroups: [resource.nvidia.com]\n" + f" resources: [computedomains]\n" + f" verbs: [get, list, watch, create, update, patch, delete]\n" + f" - apiGroups: [resource.k8s.io]\n" + f" resources: [resourceclaimtemplates]\n" + f" verbs: [get, list, watch, create, update, patch, delete]\n" f"---\n" f"apiVersion: rbac.authorization.k8s.io/v1\n" f"kind: RoleBinding\n" From 5acae62cdbfd0bb5be49fd86367d89b8574d804d Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 26 Apr 2026 16:04:20 -0700 Subject: [PATCH 67/84] fix: make --replace stop running exec jobs before resubmitting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --replace on the exec path (runMode=batch) was a no-op — the old process kept running and the new one queued behind it. Add ExecSubmitter.stop_all_running() which scans pidfiles on the head pod and SIGTERMs any live processes, then wire it into submit_training(). Signed-off-by: Terry Kong --- infra/nrl_k8s/src/nrl_k8s/orchestrate.py | 11 ++++-- infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py | 39 +++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/infra/nrl_k8s/src/nrl_k8s/orchestrate.py b/infra/nrl_k8s/src/nrl_k8s/orchestrate.py index 1b76c1ec6fd..d1d0a93984b 100644 --- a/infra/nrl_k8s/src/nrl_k8s/orchestrate.py +++ b/infra/nrl_k8s/src/nrl_k8s/orchestrate.py @@ -387,10 +387,13 @@ def submit_training( ) # `--replace` semantics: stop any running job on the training cluster - # so the new one can claim GPUs. Only applies to the Ray path; exec - # runs are keyed by run_id (unique per submission) so replace is a - # no-op there. - if replace and not is_exec: + # so the new one can claim GPUs. + if replace and is_exec: + from .submitters.exec_ import ExecSubmitter + ExecSubmitter(exec_tmp_dir=infra.submit.execTmpDir).stop_all_running( + name, infra.namespace, log=log, + ) + elif replace: with submit.dashboard_url(name, infra.namespace) as dash: client = JobSubmissionClient(dash) for job in client.list_jobs(): diff --git a/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py b/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py index cfd9e499fe0..1aa763fa014 100644 --- a/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py +++ b/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py @@ -265,6 +265,45 @@ def stop(self, handle: SubmissionHandle, *, force: bool = False) -> None: _run(["kubectl", "exec", "-n", handle.namespace, pod, "--", "bash", "-c", kill]) + def stop_all_running( + self, + cluster_name: str, + namespace: str, + *, + log: callable = lambda _: None, + ) -> None: + """Kill every live exec run on the head pod. + + Scans ``/nrl-*/pid`` for processes still alive (``kill -0``) + and sends SIGTERM. Used by ``--replace`` to free resources before + submitting a new run. + """ + pod = k8s.get_head_pod(cluster_name, namespace) + pod_name = pod.metadata.name + script = ( + f'for pidfile in {self._tmp_root}/nrl-*/pid; do ' + f' [ -f "$pidfile" ] || continue; ' + f' pid=$(cat "$pidfile"); ' + f' if kill -0 "$pid" 2>/dev/null; then ' + f' run_dir=$(dirname "$pidfile"); ' + f' run_id=$(basename "$run_dir" | sed "s/^nrl-//"); ' + f' echo "stopping $run_id (pid $pid)"; ' + f' kill -s TERM "$pid" 2>/dev/null || true; ' + f' fi; ' + f'done' + ) + try: + out = _run( + ["kubectl", "exec", "-n", namespace, pod_name, "--", + "bash", "-c", script], + capture=True, + ) + for line in out.strip().splitlines(): + if line: + log(f"[training] --replace: {line}") + except (subprocess.CalledProcessError, _ExecFailed): + log("[training] --replace: warning: could not scan for running exec jobs") + # ============================================================================= # Internals From 1da1e98badefafed9a16b3b67ae65cae35b9726b Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 26 Apr 2026 16:16:03 -0700 Subject: [PATCH 68/84] fix: kill process group (not just pid) in exec stop/replace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entrypoint spawns a process tree (bash → python, tee) that shares a PGID. Killing just the top-level PID leaves children running under init. Use kill -TERM -$pgid to take down the entire group. Signed-off-by: Terry Kong --- infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py b/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py index 1aa763fa014..107f20987a5 100644 --- a/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py +++ b/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py @@ -259,7 +259,13 @@ def stop(self, handle: SubmissionHandle, *, force: bool = False) -> None: sig = "KILL" if force else "TERM" kill = ( f'if [ -f {shlex.quote(tmp)}/pid ]; then ' - f' kill -s {sig} "$(cat {shlex.quote(tmp)}/pid)" 2>/dev/null || true; ' + f' pid=$(cat {shlex.quote(tmp)}/pid); ' + f' pgid=$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d " "); ' + f' if [ -n "$pgid" ]; then ' + f' kill -s {sig} -"$pgid" 2>/dev/null || true; ' + f' else ' + f' kill -s {sig} "$pid" 2>/dev/null || true; ' + f' fi; ' f'fi' ) _run(["kubectl", "exec", "-n", handle.namespace, pod, "--", @@ -287,8 +293,14 @@ def stop_all_running( f' if kill -0 "$pid" 2>/dev/null; then ' f' run_dir=$(dirname "$pidfile"); ' f' run_id=$(basename "$run_dir" | sed "s/^nrl-//"); ' - f' echo "stopping $run_id (pid $pid)"; ' - f' kill -s TERM "$pid" 2>/dev/null || true; ' + f' pgid=$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d " "); ' + f' if [ -n "$pgid" ]; then ' + f' echo "stopping $run_id (pgid $pgid)"; ' + f' kill -s TERM -"$pgid" 2>/dev/null || true; ' + f' else ' + f' echo "stopping $run_id (pid $pid)"; ' + f' kill -s TERM "$pid" 2>/dev/null || true; ' + f' fi; ' f' fi; ' f'done' ) From 202514b4b3a14d42c20ce123cec73f7152bae04b Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 26 Apr 2026 16:31:16 -0700 Subject: [PATCH 69/84] fix: wait for exec processes to exit before resubmitting After SIGTERM-ing the process group, wait up to 10s for all processes to actually die. This gives Ray time to notice the driver disconnected and GC actors (e.g. vLLM engines) on workers, freeing GPU memory before the new run starts. Signed-off-by: Terry Kong --- infra/nrl_k8s/src/nrl_k8s/orchestrate.py | 10 +++- infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py | 56 ++++++++++++++++--- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/infra/nrl_k8s/src/nrl_k8s/orchestrate.py b/infra/nrl_k8s/src/nrl_k8s/orchestrate.py index d1d0a93984b..e9e7b96067d 100644 --- a/infra/nrl_k8s/src/nrl_k8s/orchestrate.py +++ b/infra/nrl_k8s/src/nrl_k8s/orchestrate.py @@ -387,7 +387,15 @@ def submit_training( ) # `--replace` semantics: stop any running job on the training cluster - # so the new one can claim GPUs. + # so the new one can claim GPUs and worker actors are cleaned up. + # + # Exec path: kill the driver process *group* on the head pod. The + # python driver owns all Ray actors via ray.init(); when the driver + # dies, Ray GCs those actors (including vLLM engines on workers). + # Killing just the PID left python alive → actors orphaned. + # + # Port-forward path: use the Ray Job SDK to stop jobs, which handles + # actor cleanup on the Ray side. if replace and is_exec: from .submitters.exec_ import ExecSubmitter ExecSubmitter(exec_tmp_dir=infra.submit.execTmpDir).stop_all_running( diff --git a/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py b/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py index 107f20987a5..b858f16022d 100644 --- a/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py +++ b/infra/nrl_k8s/src/nrl_k8s/submitters/exec_.py @@ -277,16 +277,23 @@ def stop_all_running( namespace: str, *, log: callable = lambda _: None, + wait_s: int = 10, ) -> None: """Kill every live exec run on the head pod. - Scans ``/nrl-*/pid`` for processes still alive (``kill -0``) - and sends SIGTERM. Used by ``--replace`` to free resources before - submitting a new run. + Scans ``/nrl-*/pid`` for processes still alive, looks up + their process group, and sends SIGTERM to the entire group. This + ensures child processes (python driver, tee, etc.) are killed too — + once the python driver dies, Ray GCs the actors it owned on workers. + + After signalling, waits up to *wait_s* seconds for all processes to + exit so Ray has time to reclaim worker resources before the new run + starts. """ pod = k8s.get_head_pod(cluster_name, namespace) pod_name = pod.metadata.name - script = ( + kill_script = ( + f'killed=0; ' f'for pidfile in {self._tmp_root}/nrl-*/pid; do ' f' [ -f "$pidfile" ] || continue; ' f' pid=$(cat "$pidfile"); ' @@ -301,20 +308,55 @@ def stop_all_running( f' echo "stopping $run_id (pid $pid)"; ' f' kill -s TERM "$pid" 2>/dev/null || true; ' f' fi; ' + f' killed=1; ' f' fi; ' - f'done' + f'done; ' + f'echo "KILLED=$killed"' ) + killed = False try: out = _run( ["kubectl", "exec", "-n", namespace, pod_name, "--", - "bash", "-c", script], + "bash", "-c", kill_script], capture=True, ) for line in out.strip().splitlines(): - if line: + if line.startswith("KILLED="): + killed = line == "KILLED=1" + elif line: log(f"[training] --replace: {line}") except (subprocess.CalledProcessError, _ExecFailed): log("[training] --replace: warning: could not scan for running exec jobs") + return + + if not killed: + return + + log(f"[training] --replace: waiting up to {wait_s}s for processes to exit") + wait_script = ( + f'for i in $(seq 1 {wait_s}); do ' + f' alive=0; ' + f' for pidfile in {self._tmp_root}/nrl-*/pid; do ' + f' [ -f "$pidfile" ] || continue; ' + f' pid=$(cat "$pidfile"); ' + f' kill -0 "$pid" 2>/dev/null && alive=1 && break; ' + f' done; ' + f' [ "$alive" = "0" ] && echo "all processes exited" && exit 0; ' + f' sleep 1; ' + f'done; ' + f'echo "timeout: some processes still running"' + ) + try: + out = _run( + ["kubectl", "exec", "-n", namespace, pod_name, "--", + "bash", "-c", wait_script], + capture=True, + ) + for line in out.strip().splitlines(): + if line: + log(f"[training] --replace: {line}") + except (subprocess.CalledProcessError, _ExecFailed): + pass # ============================================================================= From 14516efc045c634da5a0fd2f778009f1eb4c9db3 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 26 Apr 2026 16:40:36 -0700 Subject: [PATCH 70/84] fix: use Ray dashboard to stop jobs on exec --replace path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Killing the driver process group alone doesn't reliably clean up Ray actors on workers (vLLM engines etc.) — Ray's GC depends on heartbeat timeouts. Now both exec and port-forward paths use the dashboard API to stop jobs, which properly tears down actors. The exec path still kills the driver process group as well for head-pod cleanup. Signed-off-by: Terry Kong --- infra/nrl_k8s/src/nrl_k8s/orchestrate.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/infra/nrl_k8s/src/nrl_k8s/orchestrate.py b/infra/nrl_k8s/src/nrl_k8s/orchestrate.py index e9e7b96067d..9b36842fe2a 100644 --- a/infra/nrl_k8s/src/nrl_k8s/orchestrate.py +++ b/infra/nrl_k8s/src/nrl_k8s/orchestrate.py @@ -389,19 +389,17 @@ def submit_training( # `--replace` semantics: stop any running job on the training cluster # so the new one can claim GPUs and worker actors are cleaned up. # - # Exec path: kill the driver process *group* on the head pod. The - # python driver owns all Ray actors via ray.init(); when the driver - # dies, Ray GCs those actors (including vLLM engines on workers). - # Killing just the PID left python alive → actors orphaned. - # - # Port-forward path: use the Ray Job SDK to stop jobs, which handles - # actor cleanup on the Ray side. - if replace and is_exec: - from .submitters.exec_ import ExecSubmitter - ExecSubmitter(exec_tmp_dir=infra.submit.execTmpDir).stop_all_running( - name, infra.namespace, log=log, - ) - elif replace: + # Always go through the Ray dashboard to stop jobs — this is the only + # reliable way to tear down actors on workers (vLLM engines, etc.). + # Killing just the driver process leaves Ray actors orphaned until + # the heartbeat timeout. The exec path additionally kills the driver + # process group on the head pod. + if replace: + if is_exec: + from .submitters.exec_ import ExecSubmitter + ExecSubmitter(exec_tmp_dir=infra.submit.execTmpDir).stop_all_running( + name, infra.namespace, log=log, + ) with submit.dashboard_url(name, infra.namespace) as dash: client = JobSubmissionClient(dash) for job in client.list_jobs(): From ba3822bf6e86b5547ef312f56d1d9e0337e2bb48 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Mon, 27 Apr 2026 10:05:13 -0700 Subject: [PATCH 71/84] fix: remove unused import, add krew to PATH before install - Remove unused 'import inspect as ins' in cli.py _tail_daemon. - Add PATH export before krew plugin installs in get-kubectl.sh so krew is reachable on a fresh machine. Signed-off-by: Terry Kong --- infra/kind/get-kubectl.sh | 2 ++ infra/nrl_k8s/src/nrl_k8s/cli.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/infra/kind/get-kubectl.sh b/infra/kind/get-kubectl.sh index c02d38e9959..0704cedea13 100644 --- a/infra/kind/get-kubectl.sh +++ b/infra/kind/get-kubectl.sh @@ -57,6 +57,8 @@ else echo "krew already installed" fi +export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH" + $NAMED_KUBECTL krew install ctx $NAMED_KUBECTL krew install ns $NAMED_KUBECTL krew install stern diff --git a/infra/nrl_k8s/src/nrl_k8s/cli.py b/infra/nrl_k8s/src/nrl_k8s/cli.py index 462319bdbdc..1873273a055 100644 --- a/infra/nrl_k8s/src/nrl_k8s/cli.py +++ b/infra/nrl_k8s/src/nrl_k8s/cli.py @@ -1616,7 +1616,6 @@ def _tail(dashboard: str, job_id: str) -> None: def _tail_daemon(cluster_name: str, namespace: str, submission_id: str) -> None: """Open a dashboard port-forward and tail a Ray Job by submission_id.""" - from . import inspect as ins from . import submit as submit_mod try: From 3d846a681b542f8de6b8ef45f80ca7f61a4ac496 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Mon, 27 Apr 2026 11:08:25 -0700 Subject: [PATCH 72/84] chore: add NVIDIA copyright headers to test files Signed-off-by: Terry Kong --- infra/nrl_k8s/tests/__init__.py | 14 ++++++++++++++ infra/nrl_k8s/tests/unit/__init__.py | 14 ++++++++++++++ infra/nrl_k8s/tests/unit/test_cli.py | 14 ++++++++++++++ infra/nrl_k8s/tests/unit/test_config.py | 14 ++++++++++++++ infra/nrl_k8s/tests/unit/test_inspect.py | 14 ++++++++++++++ infra/nrl_k8s/tests/unit/test_k8s.py | 14 ++++++++++++++ infra/nrl_k8s/tests/unit/test_manifest.py | 14 ++++++++++++++ infra/nrl_k8s/tests/unit/test_orchestrate.py | 14 ++++++++++++++ infra/nrl_k8s/tests/unit/test_rayjob.py | 14 ++++++++++++++ infra/nrl_k8s/tests/unit/test_schema.py | 14 ++++++++++++++ infra/nrl_k8s/tests/unit/test_submit.py | 14 ++++++++++++++ infra/nrl_k8s/tests/unit/test_workdir.py | 14 ++++++++++++++ 12 files changed, 168 insertions(+) diff --git a/infra/nrl_k8s/tests/__init__.py b/infra/nrl_k8s/tests/__init__.py index e69de29bb2d..0338d13d2e1 100644 --- a/infra/nrl_k8s/tests/__init__.py +++ b/infra/nrl_k8s/tests/__init__.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/infra/nrl_k8s/tests/unit/__init__.py b/infra/nrl_k8s/tests/unit/__init__.py index e69de29bb2d..0338d13d2e1 100644 --- a/infra/nrl_k8s/tests/unit/__init__.py +++ b/infra/nrl_k8s/tests/unit/__init__.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/infra/nrl_k8s/tests/unit/test_cli.py b/infra/nrl_k8s/tests/unit/test_cli.py index 7593e400b0a..7f75480262a 100644 --- a/infra/nrl_k8s/tests/unit/test_cli.py +++ b/infra/nrl_k8s/tests/unit/test_cli.py @@ -1,3 +1,17 @@ +# 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. + """Tests for :mod:`nrl_k8s.cli` — click entrypoints. Use ``click.testing.CliRunner`` to invoke commands; every downstream diff --git a/infra/nrl_k8s/tests/unit/test_config.py b/infra/nrl_k8s/tests/unit/test_config.py index 0073bda4356..c6b0f386ed3 100644 --- a/infra/nrl_k8s/tests/unit/test_config.py +++ b/infra/nrl_k8s/tests/unit/test_config.py @@ -1,3 +1,17 @@ +# 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. + """Tests for :mod:`nrl_k8s.config` — layered loading of recipe + infra:. Priority (low → high): diff --git a/infra/nrl_k8s/tests/unit/test_inspect.py b/infra/nrl_k8s/tests/unit/test_inspect.py index 638c524aa53..a85af071762 100644 --- a/infra/nrl_k8s/tests/unit/test_inspect.py +++ b/infra/nrl_k8s/tests/unit/test_inspect.py @@ -1,3 +1,17 @@ +# 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. + """Tests for :mod:`nrl_k8s.inspect` — read-only introspection of clusters.""" from __future__ import annotations diff --git a/infra/nrl_k8s/tests/unit/test_k8s.py b/infra/nrl_k8s/tests/unit/test_k8s.py index 988a8532297..8991b5b5580 100644 --- a/infra/nrl_k8s/tests/unit/test_k8s.py +++ b/infra/nrl_k8s/tests/unit/test_k8s.py @@ -1,3 +1,17 @@ +# 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. + """Tests for :mod:`nrl_k8s.k8s` — thin wrapper around the official k8s client. All tests mock ``kubernetes.client`` and ``kubernetes.config`` so they never diff --git a/infra/nrl_k8s/tests/unit/test_manifest.py b/infra/nrl_k8s/tests/unit/test_manifest.py index a44825efbfb..ffe04ac87d2 100644 --- a/infra/nrl_k8s/tests/unit/test_manifest.py +++ b/infra/nrl_k8s/tests/unit/test_manifest.py @@ -1,3 +1,17 @@ +# 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. + """Tests for :mod:`nrl_k8s.manifest` — RayCluster manifest builder.""" from __future__ import annotations diff --git a/infra/nrl_k8s/tests/unit/test_orchestrate.py b/infra/nrl_k8s/tests/unit/test_orchestrate.py index 99bde3c01e0..9a0159fd81f 100644 --- a/infra/nrl_k8s/tests/unit/test_orchestrate.py +++ b/infra/nrl_k8s/tests/unit/test_orchestrate.py @@ -1,3 +1,17 @@ +# 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. + """Tests for :mod:`nrl_k8s.orchestrate` — the bring-up / submit pipeline. Every external system is stubbed: no Kubernetes API, no Ray dashboard, no diff --git a/infra/nrl_k8s/tests/unit/test_rayjob.py b/infra/nrl_k8s/tests/unit/test_rayjob.py index c99ce96f8dc..c8e7d63219f 100644 --- a/infra/nrl_k8s/tests/unit/test_rayjob.py +++ b/infra/nrl_k8s/tests/unit/test_rayjob.py @@ -1,3 +1,17 @@ +# 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. + """Tests for :mod:`nrl_k8s.rayjob` — RayJob manifest builder.""" from __future__ import annotations diff --git a/infra/nrl_k8s/tests/unit/test_schema.py b/infra/nrl_k8s/tests/unit/test_schema.py index b92d5e025d8..3088f968ab5 100644 --- a/infra/nrl_k8s/tests/unit/test_schema.py +++ b/infra/nrl_k8s/tests/unit/test_schema.py @@ -1,3 +1,17 @@ +# 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. + """Tests for :mod:`nrl_k8s.schema`. The schema is the contract between recipes and every downstream template, so diff --git a/infra/nrl_k8s/tests/unit/test_submit.py b/infra/nrl_k8s/tests/unit/test_submit.py index 9c61ec9213d..e2170f4ff3e 100644 --- a/infra/nrl_k8s/tests/unit/test_submit.py +++ b/infra/nrl_k8s/tests/unit/test_submit.py @@ -1,3 +1,17 @@ +# 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. + """Tests for :mod:`nrl_k8s.submit` — dashboard access + Ray job submission. ``dashboard_url`` has two branches (in-cluster DNS vs. laptop port-forward) diff --git a/infra/nrl_k8s/tests/unit/test_workdir.py b/infra/nrl_k8s/tests/unit/test_workdir.py index 5dee673cdb9..5ff4a772ad5 100644 --- a/infra/nrl_k8s/tests/unit/test_workdir.py +++ b/infra/nrl_k8s/tests/unit/test_workdir.py @@ -1,3 +1,17 @@ +# 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. + """Tests for :mod:`nrl_k8s.workdir` — staging the Ray ``working_dir`` upload. These tests build a small fixture repo tree under ``tmp_path`` and verify: From 7535d6668daefe5ae031dab6455cd145d94e26b9 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 22:24:43 -0700 Subject: [PATCH 73/84] chore: re-add qwen3_4b examples (non-upstream) Signed-off-by: Terry Kong --- .../qwen3_4b_if_full_disagg.infra.yaml | 386 ++++++++++++++++++ .../examples/qwen3_4b_if_full_disagg.yaml | 33 ++ .../qwen3_4b_if_gym_disagg.infra.yaml | 222 ++++++++++ .../qwen3_4b_if_gym_disagg.prod.infra.yaml | 244 +++++++++++ .../examples/qwen3_4b_if_gym_disagg.yaml | 49 +++ .../qwen3_4b_if_single.gb300.infra.yaml | 150 +++++++ .../qwen3_4b_if_single.gb300.prod.infra.yaml | 181 ++++++++ .../examples/qwen3_4b_if_single.infra.yaml | 169 ++++++++ .../nrl_k8s/examples/qwen3_4b_if_single.yaml | 50 +++ 9 files changed, 1484 insertions(+) create mode 100644 infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml create mode 100644 infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml create mode 100644 infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml create mode 100644 infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml create mode 100644 infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml create mode 100644 infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml create mode 100644 infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml create mode 100644 infra/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml create mode 100644 infra/nrl_k8s/examples/qwen3_4b_if_single.yaml diff --git a/infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml b/infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml new file mode 100644 index 00000000000..c9dd942b71f --- /dev/null +++ b/infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml @@ -0,0 +1,386 @@ +# Infra for the Qwen3-4B full-disaggregated run — paired with qwen3_4b_if_full_disagg.yaml. +# +# Usage: +# nrl-k8s run tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml +# +# Everything K8s-specific lives here so the recipe stays cluster-agnostic. +# Shared YAML anchors below keep the three cluster specs from repeating. + +# ============================================================================= +# Shared anchors — consumed during YAML parse. +# ============================================================================= +_shared: + nodeSelector: &shared_node_selector + gpu-wrangler.nvidia.com/lease: nemo-rl-testing + tolerations: &shared_tolerations + - {key: gpu-wrangler.nvidia.com/lease, operator: Equal, value: nemo-rl-testing, effect: NoSchedule} + - {key: platform.nvidia.com/gpu, operator: Equal, value: "true", effect: NoSchedule} + # Keep pod-spec env minimal — the nemo-rl:nightly image already sets + # NCCL_NET_PLUGIN=aws-ofi, NCCL_SOCKET_IFNAME=enp71s0, etc. in its shell + # init. Overriding them here just causes Ray runtime-env merge conflicts + # when ray.init() captures os.environ inside the job entrypoint. + headEnv: &shared_head_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + workerEnv: &shared_worker_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + headResources: &shared_head_resources + limits: {cpu: "8", memory: "32Gi"} + requests: {cpu: "2", memory: "8Gi"} + gpuWorkerResources: &gpu_worker_resources + # Sized for p5.48xlarge (192 CPU / ~1957 GiB allocatable). We claim + # most of the node and leave ~16 CPU + ~150 GiB headroom for: + # - the training head pod colocated here (~8 CPU / 32 GiB) + # - a gym head pod if it lands on this node (~8 CPU / 32 GiB) + # - daemonsets: kube-proxy, aws-node, nvidia-device-plugin, + # node-exporter (~4 CPU / 16 GiB total) + # p5.48xlarge also exposes 32 EFA-capable ENIs. The AWS OFI NCCL plugin + # (bundled in the image as NCCL_NET_PLUGIN=aws-ofi) needs them mapped + # into the container as `vpc.amazonaws.com/efa` resources — the EFA + # device plugin on the node injects the character devices under + # /dev/infiniband/ and the libfabric provider discovers them at + # startup. Drop the EFA request and you'll see `FI_PROVIDER=efa` log + # "No usable providers found" and NCCL fall back to socket / hang. + # Also: `nvidia.com/gpu: "8"` must equal the rayStartParams num-gpus + # on the same worker template, otherwise Ray's resource view disagrees + # with what CUDA actually sees. + limits: + cpu: "176" + memory: "1800Gi" + nvidia.com/gpu: "8" + vpc.amazonaws.com/efa: "32" + requests: + cpu: "176" + memory: "1800Gi" + nvidia.com/gpu: "8" + vpc.amazonaws.com/efa: "32" + gpuHeadPorts: &gpu_head_ports + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + dshmMounts: &dshm_mounts + - {mountPath: /dev/shm, name: dshm} + dshm16: &dshm_16 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 16Gi} + dshm64: &dshm_64 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 64Gi} + dshm4: &dshm_4 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 4Gi} + +# ============================================================================= +# Top-level InfraConfig (no wrapping `infra:` needed — the CLI accepts either) +# ============================================================================= +namespace: nemo-rl-testing +image: nvcr.io/nvidian/nemo-rl:nightly +imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] +serviceAccount: nemo-rl-endpoint-registry + +labels: + nrl-k8s/owner: ${user:} + +launch: + mode: attach + attach: + generation: ${user:}-raycluster-generation-qwen3-4b + gym: ${user:}-raycluster-gym-qwen3-4b + training: ${user:}-raycluster-rl-qwen3-4b + peerWatcher: false + # Minimal upload set — Ray caps working_dir at 100 MiB. We include only + # what this run needs: nemo_rl, examples, the nemo_gym subset, and the + # instruction_following data (~90 MiB, fits under the cap). + rayUploadPaths: + - nemo_rl + - examples + - infra/examples + - tests/check_metrics.py + - tests/json_dump_tb_logs.py + - 3rdparty/Gym-workspace/Gym/nemo_gym + - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model + - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent + - 3rdparty/Gym-workspace/Gym/pyproject.toml + - 3rdparty/Gym-workspace/Gym/uv.lock + # Only configs (not data/*.jsonl) — the big train/validation jsonls are + # pre-staged via kubectl cp onto the training pods' /tmp (see env above). + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs + # nemo_rl.distributed.model_utils imports megatron.core. + - 3rdparty/Megatron-LM-workspace/Megatron-LM/megatron + # The CLI stages the merged recipe as ./nrl_k8s_run.yaml at the working_dir + # root — reference it by name from the entrypoint. + entrypoint: | + # kubernetes client is required by K8sEndpointRegistry (used by training + # to read vllm_base_urls from the ConfigMap). The image *usually* has it, + # but installing here is cheap insurance and keeps the recipe portable. + pip install kubernetes -q || true + # Inline exports — see note on launch.env below. + # GLOO_SOCKET_IFNAME: PyTorch distributed gloo backend (used for the + # rendezvous store). Default eth0 isn't the interface on p5 hosts. + export GLOO_SOCKET_IFNAME=enp71s0 + # NCCL_SOCKET_IFNAME: control-plane interface NCCL uses before OFI + # takes over data. Must match the host's primary AWS VPC ENI. + export NCCL_SOCKET_IFNAME=enp71s0 + # FI_PROVIDER: tells libfabric (and thus aws-ofi-nccl) to use EFA. + # Without it you'd get the TCP provider over eth0, ~10x slower. + export FI_PROVIDER=efa + # Data paths — files were pre-staged on the pods via kubectl cp onto + # each training pod's /tmp (the 87 MB jsonl blows the 100 MiB + # working_dir cap, so Ray can't ship it). + export DISAGG_TRAIN_PATH=/tmp/train.jsonl + export DISAGG_VALID_PATH=/tmp/validation.jsonl + # expandable_segments reduces CUDA memory fragmentation under varying + # batch shapes. Safe to set in disagg mode; UNSAFE in colocated mode + # (vLLM's CuMemAllocator asserts). The single-cluster recipe omits it. + export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True + # Kill Ray tasks that can never schedule instead of letting them + # queue forever — surfaces cluster-capacity bugs fast. + export RAY_enable_infeasible_task_early_exit=true + # Make sibling workspaces importable alongside the repo root. + export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM + # disagg_job_id is passed in three places — here (training), + # the gen daemon export DISAGG_JOB_ID (generation), and the gym daemon's + # --job-id flag (gym). It is deliberately NOT an InfraConfig field: the + # CLI infers the ConfigMap name by regex-parsing --job-id out of the gym + # entrypoint (orchestrate.py:_infer_disagg_job_id), so adding a separate + # key would just duplicate information and let the two go out of sync. + # Keep all three spellings in sync when you rename a run. + python -u examples/nemo_gym/run_grpo_nemo_gym.py \ + --config nrl_k8s_run.yaml \ + +policy.generation.remote_generation_url=http://raycluster-generation-qwen3-4b-head-svc.nemo-rl-testing.svc.cluster.local:8089 \ + +env.disagg_job_id=qwen3-4b-if-gym \ + ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ + ~policy.optimizer.kwargs.foreach \ + ~policy.optimizer.kwargs.fused + # All env flows through inline `export`s in the entrypoint — setting + # anything here puts it in runtime_env.env_vars, which can't merge with + # ray.init's captured env_vars and aborts with "Failed to merge". + env: {} + +clusters: + # ------------------------------------------------------------------------- + # Generation — GPU head + worker, control-server on 8089. + # ------------------------------------------------------------------------- + generation: + name: ${user:}-raycluster-generation-qwen3-4b + labels: + disagg.nemo-rl/cluster: generation-qwen3-4b + disagg.nemo-rl/run: qwen3-4b-gym + daemon: + submissionId: qwen3-4b-generation-server-v7 + # Inline exports (not runtime_env.env_vars, which conflicts with + # ray.init's captured env). The Python process calls ray.init() after + # these are exported, ray captures them into its env, and propagates + # them to the worker actors + their subprocesses — so vLLM's + # EngineCore sees GLOO_SOCKET_IFNAME=enp71s0 and doesn't try eth0. + # DISAGG_JOB_ID triggers the K8sEndpointRegistry publish path so + # gym can discover the gen shards via the ConfigMap. + entrypoint: | + export GLOO_SOCKET_IFNAME=enp71s0 + export NCCL_SOCKET_IFNAME=enp71s0 + export FI_PROVIDER=efa + export DISAGG_JOB_ID=qwen3-4b-if-gym + # K8sEndpointRegistry needs the `kubernetes` client. + pip install kubernetes -q || true + python -u examples/run_standalone_generation_server.py \ + --config infra/examples/generation_standalone_qwen3_4b_dapo.yaml \ + --port 8089 --num-gpus 8 + # Gen server doesn't need training data — keep the upload small. + rayUploadPaths: + - nemo_rl + - examples + - infra/examples + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-head + env: *shared_head_env + resources: *shared_head_resources + ports: + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + - {containerPort: 8089, name: control-server} + volumeMounts: *dshm_mounts + volumes: *dshm_16 + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: {num-gpus: "8", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + # hostNetwork=true on GPU workers is required for EFA: the + # AWS OFI NCCL plugin opens raw libfabric endpoints bound to + # the host ENI (enp71s0, above), which a pod network + # namespace can't see. With the default CNI network the + # plugin initialises but communication never completes and + # NCCL hangs at the first AllReduce. `dnsPolicy: + # ClusterFirstWithHostNet` then restores in-cluster DNS so + # the worker can still resolve the head svc and the gen + # server's FQDN. + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-worker + env: *shared_worker_env + resources: *gpu_worker_resources + volumeMounts: *dshm_mounts + volumes: *dshm_64 + + # ------------------------------------------------------------------------- + # Gym — CPU-only head, gym-server on 9090. + # ------------------------------------------------------------------------- + gym: + name: ${user:}-raycluster-gym-qwen3-4b + daemon: + submissionId: qwen3-4b-if-gym-server-v7 + # Ray runs entrypoints under /bin/dash. We stay POSIX here (no + # pipefail, no process substitution). OmegaConf ${...} escaped as + # \${...} — Python dollar-refs for ray.__version__ use a one-liner. + entrypoint: | + set -eu + ROOT_DIR=$(pwd) + GYM_DIR=\${ROOT_DIR}/3rdparty/Gym-workspace/Gym + export PYTHONPATH=\${ROOT_DIR}:\${GYM_DIR}:\${PYTHONPATH-} + cd \${GYM_DIR} + mkdir -p cache + RAY_VERSION=$(python -c "import ray; print(ray.__version__)") + echo "ray==\${RAY_VERSION}" > /tmp/ray-pin.txt + pip install -e "." --constraint /tmp/ray-pin.txt -q + pip install kubernetes -q + exec python -m nemo_gym.standalone_server \ + --job-id qwen3-4b-if-gym \ + --port 9090 \ + --model-name Qwen/Qwen3-4B-Instruct-2507 \ + --config-yaml \${ROOT_DIR}/infra/examples/gym_standalone_config_instruction_following.yaml + # Gym needs Gym code + infra configs + the nemo_rl.distributed package + # for the k8s endpoint registry. We list instruction_following files + # individually instead of the whole resources_servers/instruction_following + # directory because that directory holds an 87 MB train.jsonl — Ray's + # 100 MiB working_dir cap would kick in and submission would fail. + # Listing app.py and requirements.txt by name picks up the FastAPI + # resource server and the rubric-grader wheels at daemon start + # (the `pip install -e "."` above reads them) without shipping the + # bulk dataset. The smaller validation.jsonl / example.jsonl / etc. + # are included because gym's smoke tests and rollout warm-up read + # them, and together they're under 2 MiB. + rayUploadPaths: + - nemo_rl + - infra/examples + - 3rdparty/Gym-workspace/Gym/nemo_gym + - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model + - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent + - 3rdparty/Gym-workspace/Gym/pyproject.toml + - 3rdparty/Gym-workspace/Gym/uv.lock + # app.py: the instruction_following FastAPI resource server entry. + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/app.py + # requirements.txt: rubric-grader deps gym installs on daemon start. + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/requirements.txt + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/tests + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/validation.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example_rollouts.jsonl + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {num-gpus: "0", dashboard-host: "0.0.0.0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-head + env: + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + resources: *shared_head_resources + ports: + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + - {containerPort: 9090, name: gym-server} + volumeMounts: *dshm_mounts + volumes: *dshm_4 + + # ------------------------------------------------------------------------- + # Training — GPU head + worker. Head has WANDB secret. + # ------------------------------------------------------------------------- + training: + name: ${user:}-raycluster-rl-qwen3-4b + labels: + disagg.nemo-rl/cluster: rl-qwen3-4b + disagg.nemo-rl/run: qwen3-4b-gym + # No daemon: waits for `nrl-k8s run` / `nrl-k8s rayjob`. + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-head + env: + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: GLOO_SOCKET_IFNAME, value: eth0} + - {name: NCCL_SOCKET_IFNAME, value: eth0} + - {name: NCCL_DEBUG, value: INFO} + - {name: NCCL_IB_DISABLE, value: "1"} + - {name: NCCL_NET, value: Socket} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: + name: wandb-api-key + key: WANDB_API_KEY + resources: *shared_head_resources + ports: *gpu_head_ports + volumeMounts: *dshm_mounts + volumes: *dshm_16 + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: {num-gpus: "8", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + # Same EFA + NCCL reasoning as the generation worker above. + # Training AllReduces run across these workers via aws-ofi; + # without hostNetwork they hang at the first collective. + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-worker + env: *shared_worker_env + resources: *gpu_worker_resources + volumeMounts: *dshm_mounts + volumes: *dshm_64 diff --git a/infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml b/infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml new file mode 100644 index 00000000000..8c7d1b545f6 --- /dev/null +++ b/infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml @@ -0,0 +1,33 @@ +# Recipe: Qwen3-4B disaggregated GRPO on instruction_following gym. +# +# Pure NeMo-RL recipe — no infra. Runs in any environment. Pair with an infra +# file via ``nrl-k8s run qwen3_4b_if.yaml --infra qwen3_4b_if.infra.yaml``. +defaults: ../../../examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml + +cluster: + gpus_per_node: 8 + num_nodes: 1 +grpo: + num_prompts_per_step: 32 + num_generations_per_prompt: 16 + max_num_steps: 200 + val_period: 50 + max_rollout_turns: 1 + async_grpo: + max_trajectory_age_steps: 1 +policy: + train_global_batch_size: 512 + train_micro_batch_size: 1 + logprob_batch_size: 1 + max_total_sequence_length: 16384 + dtensor_cfg: + activation_checkpointing: true +checkpointing: + save_period: 50 +logger: + wandb_enabled: true + wandb: + project: nemorl-full-disagg-k8s + name: full-disagg-if-qwen3-4b + tensorboard_enabled: true + monitor_gpus: true diff --git a/infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml b/infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml new file mode 100644 index 00000000000..3794350e956 --- /dev/null +++ b/infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.infra.yaml @@ -0,0 +1,222 @@ +# Infra for gym-disaggregated Qwen3-4B colocated GRPO run. +# +# Two RayClusters: a single GPU cluster for training (with generation +# colocated inside it) and the usual CPU-only gym cluster. No separate +# generation cluster. + +_shared: + nodeSelector: &shared_node_selector + gpu-wrangler.nvidia.com/lease: nemo-rl-testing + tolerations: &shared_tolerations + - {key: gpu-wrangler.nvidia.com/lease, operator: Equal, value: nemo-rl-testing, effect: NoSchedule} + - {key: platform.nvidia.com/gpu, operator: Equal, value: "true", effect: NoSchedule} + # Image ships aws-ofi + enp71s0 defaults; keep pod env minimal. + headEnv: &shared_head_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + workerEnv: &shared_worker_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + headResources: &shared_head_resources + limits: {cpu: "8", memory: "32Gi"} + requests: {cpu: "2", memory: "8Gi"} + gpuWorkerResources: &gpu_worker_resources + # Sized for p5.48xlarge (192 CPU / ~1957 GiB allocatable). Claim most + # of the node; leave ~16 CPU + ~150 GiB for the training head, a + # colocated gym head, and k8s daemonsets (kube-proxy, aws-node, + # nvidia-device-plugin, node-exporter). + limits: + cpu: "176" + memory: "1800Gi" + nvidia.com/gpu: "8" + vpc.amazonaws.com/efa: "32" + requests: + cpu: "176" + memory: "1800Gi" + nvidia.com/gpu: "8" + vpc.amazonaws.com/efa: "32" + gpuHeadPorts: &gpu_head_ports + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + dshmMounts: &dshm_mounts + - {mountPath: /dev/shm, name: dshm} + dshm16: &dshm_16 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 16Gi} + dshm64: &dshm_64 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 64Gi} + dshm4: &dshm_4 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 4Gi} + +namespace: nemo-rl-testing +image: nvcr.io/nvidian/nemo-rl:nightly +imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] +serviceAccount: nemo-rl-endpoint-registry + +labels: + nrl-k8s/owner: ${user:} + +launch: + mode: attach + attach: + gym: ${user:}-raycluster-gym-disagg-gym-qwen3-4b + training: ${user:}-raycluster-gym-disagg-qwen3-4b + peerWatcher: false + # Colocated generation → no --remote_generation_url override. + # All env flows through inline exports (runtime_env.env_vars merge-fails + # when ray.init also has env_vars). + entrypoint: | + pip install kubernetes -q || true + export GLOO_SOCKET_IFNAME=enp71s0 + export NCCL_SOCKET_IFNAME=enp71s0 + export FI_PROVIDER=efa + # Data files are pre-staged on the pods via kubectl cp. + export DISAGG_TRAIN_PATH=/tmp/train.jsonl + export DISAGG_VALID_PATH=/tmp/validation.jsonl + # NOTE: PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True is incompatible + # with colocated vLLM's CuMemAllocator (AssertionError at vLLM init). + # Disagg sets it because gen doesn't use the memory pool; single does. + export RAY_enable_infeasible_task_early_exit=true + export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM + python -u examples/nemo_gym/run_grpo_nemo_gym.py \ + --config nrl_k8s_run.yaml \ + +env.disagg_job_id=qwen3-4b-gym-disagg \ + ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ + ~policy.optimizer.kwargs.foreach \ + ~policy.optimizer.kwargs.fused + env: {} + rayUploadPaths: + - nemo_rl + - examples + - infra/examples + - tests/check_metrics.py + - tests/json_dump_tb_logs.py + - 3rdparty/Gym-workspace/Gym/nemo_gym + - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model + - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent + - 3rdparty/Gym-workspace/Gym/pyproject.toml + - 3rdparty/Gym-workspace/Gym/uv.lock + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs + - 3rdparty/Megatron-LM-workspace/Megatron-LM/megatron + +clusters: + # ---------------- Gym (CPU, head-only) ---------------- + gym: + name: ${user:}-raycluster-gym-disagg-gym-qwen3-4b + daemon: + submissionId: qwen3-4b-gym-disagg-server-v5 + entrypoint: | + set -eu + ROOT_DIR=$(pwd) + GYM_DIR=\${ROOT_DIR}/3rdparty/Gym-workspace/Gym + export PYTHONPATH=\${ROOT_DIR}:\${GYM_DIR}:\${PYTHONPATH-} + cd \${GYM_DIR} + mkdir -p cache + RAY_VERSION=$(python -c "import ray; print(ray.__version__)") + echo "ray==\${RAY_VERSION}" > /tmp/ray-pin.txt + pip install -e "." --constraint /tmp/ray-pin.txt -q + pip install kubernetes -q + # In single-cluster mode training (run_grpo_nemo_gym.py) publishes + # vllm_base_urls to the endpoint registry once colocated vLLM spawns. + # Gym blocks on that key until training publishes — a two-way rendezvous. + exec python -m nemo_gym.standalone_server \ + --job-id qwen3-4b-gym-disagg \ + --port 9090 \ + --model-name Qwen/Qwen3-4B-Instruct-2507 \ + --config-yaml \${ROOT_DIR}/infra/examples/gym_standalone_config_instruction_following.yaml + rayUploadPaths: + - nemo_rl + - infra/examples + - 3rdparty/Gym-workspace/Gym/nemo_gym + - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model + - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent + - 3rdparty/Gym-workspace/Gym/pyproject.toml + - 3rdparty/Gym-workspace/Gym/uv.lock + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/app.py + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/requirements.txt + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/tests + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/validation.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example_rollouts.jsonl + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {num-gpus: "0", dashboard-host: "0.0.0.0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-head + env: + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + resources: *shared_head_resources + ports: + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + - {containerPort: 9090, name: gym-server} + volumeMounts: *dshm_mounts + volumes: *dshm_4 + + # ---------------- Training (GPU head + worker, generation colocated) ---------------- + training: + name: ${user:}-raycluster-gym-disagg-qwen3-4b + labels: + disagg.nemo-rl/cluster: gym-disagg-qwen3-4b + disagg.nemo-rl/run: qwen3-4b-gym-disagg + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-head + env: + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: + name: wandb-api-key + key: WANDB_API_KEY + resources: *shared_head_resources + ports: *gpu_head_ports + volumeMounts: *dshm_mounts + volumes: *dshm_16 + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: {num-gpus: "8", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-worker + env: *shared_worker_env + resources: *gpu_worker_resources + volumeMounts: *dshm_mounts + volumes: *dshm_64 diff --git a/infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml b/infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml new file mode 100644 index 00000000000..86adb1b23a9 --- /dev/null +++ b/infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml @@ -0,0 +1,244 @@ +# Production-batch variant of qwen3_4b_if_gym_disagg.infra.yaml. +# +# Same recipe, same two RayClusters (training + gym), same runtime +# behaviour — but submission goes through `kubectl exec` into the +# training head pod and the code lives at /opt/nemo-rl inside the +# image. No working_dir upload; the submitting laptop can disconnect +# as soon as nohup fires. +# +# Usage: +# # Admins, once: bring up the two RayClusters. +# nrl-k8s cluster up tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ +# --role gym +# nrl-k8s cluster up tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ +# --role training +# +# # Researcher, per run — idempotent; reuses live clusters if matching, +# # submits training against the training cluster. Returns in seconds, +# # laptop disconnectable. Use --skip-daemons after first bring-up if +# # gym/generation are already healthy. +# nrl-k8s run tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ +# --run-id qwen3-4b-gym-disagg-$(date +%Y%m%d-%H%M%S) +# +# # Observe from anywhere; handle is cached on disk. +# nrl-k8s job logs \ +# tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ +# --role training -f + +_shared: + nodeSelector: &shared_node_selector + gpu-wrangler.nvidia.com/lease: nemo-rl-testing + tolerations: &shared_tolerations + - {key: gpu-wrangler.nvidia.com/lease, operator: Equal, value: nemo-rl-testing, effect: NoSchedule} + - {key: platform.nvidia.com/gpu, operator: Equal, value: "true", effect: NoSchedule} + headEnv: &shared_head_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + workerEnv: &shared_worker_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + headResources: &shared_head_resources + limits: {cpu: "8", memory: "32Gi"} + requests: {cpu: "2", memory: "8Gi"} + gpuWorkerResources: &gpu_worker_resources + limits: + cpu: "176" + memory: "1800Gi" + nvidia.com/gpu: "8" + vpc.amazonaws.com/efa: "32" + requests: + cpu: "176" + memory: "1800Gi" + nvidia.com/gpu: "8" + vpc.amazonaws.com/efa: "32" + gpuHeadPorts: &gpu_head_ports + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + dshmMounts: &dshm_mounts + - {mountPath: /dev/shm, name: dshm} + dshm16: &dshm_16 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 16Gi} + dshm64: &dshm_64 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 64Gi} + dshm4: &dshm_4 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 4Gi} + +namespace: nemo-rl-testing +image: nvcr.io/nvidian/nemo-rl:nightly +imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] +serviceAccount: nemo-rl-endpoint-registry + +labels: + nrl-k8s/owner: ${user:} + +submit: + # Shell into the training head pod instead of port-forwarding the + # dashboard to the laptop. Once `nohup` + `disown` fire the laptop is + # off the critical path. + submitter: exec + execTmpDir: /tmp + +launch: + mode: attach + # runMode: batch flips defaults without --mode on the CLI. + runMode: batch + # Code is baked into the container at /opt/nemo-rl (the + # nvcr.io/nvidian/nemo-rl:nightly build ships it). No working_dir + # upload, no .gitignore strip, no 100 MiB cap. + codeSource: image + codePath: /opt/nemo-rl + attach: + gym: ${user:}-raycluster-gym-disagg-gym-qwen3-4b + training: ${user:}-raycluster-gym-disagg-qwen3-4b + peerWatcher: false + env: {} + # Entrypoint runs inside the training head pod under `nohup bash`. + # Env vars here propagate to Ray's worker actors via the standard + # os.environ capture — no runtime_env.env_vars gymnastics. + entrypoint: | + # Keep POSIX-compatible — Ray's port-forward path submits entrypoints + # through /bin/dash, which doesn't support `set -o pipefail`. + set -eu + cd /opt/nemo-rl + export GLOO_SOCKET_IFNAME=enp71s0 + export NCCL_SOCKET_IFNAME=enp71s0 + export FI_PROVIDER=efa + export DISAGG_TRAIN_PATH=/tmp/train.jsonl + export DISAGG_VALID_PATH=/tmp/validation.jsonl + export RAY_enable_infeasible_task_early_exit=true + export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM + + python -u examples/nemo_gym/run_grpo_nemo_gym.py \ + --config examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml \ + +env.disagg_job_id=qwen3-4b-gym-disagg \ + ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ + ~policy.optimizer.kwargs.foreach \ + ~policy.optimizer.kwargs.fused + +clusters: + # ---------------- Gym (CPU, head-only) ---------------- + # Gym daemon still submits via port-forward + working_dir upload — + # it's a long-lived server admins bring up once, so the upload cost + # is paid once per `cluster up` and the researcher's `launch` never + # touches it. + gym: + name: ${user:}-raycluster-gym-disagg-gym-qwen3-4b + daemon: + submissionId: qwen3-4b-gym-disagg-server-v5 + entrypoint: | + set -eu + ROOT_DIR=$(pwd) + GYM_DIR=\${ROOT_DIR}/3rdparty/Gym-workspace/Gym + export PYTHONPATH=\${ROOT_DIR}:\${GYM_DIR}:\${PYTHONPATH-} + cd \${GYM_DIR} + mkdir -p cache + RAY_VERSION=$(python -c "import ray; print(ray.__version__)") + echo "ray==\${RAY_VERSION}" > /tmp/ray-pin.txt + pip install -e "." --constraint /tmp/ray-pin.txt -q + pip install kubernetes -q + exec python -m nemo_gym.standalone_server \ + --job-id qwen3-4b-gym-disagg \ + --port 9090 \ + --model-name Qwen/Qwen3-4B-Instruct-2507 \ + --config-yaml \${ROOT_DIR}/infra/examples/gym_standalone_config_instruction_following.yaml + rayUploadPaths: + - nemo_rl + - infra/examples + - 3rdparty/Gym-workspace/Gym/nemo_gym + - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model + - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent + - 3rdparty/Gym-workspace/Gym/pyproject.toml + - 3rdparty/Gym-workspace/Gym/uv.lock + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/app.py + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/requirements.txt + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/tests + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/validation.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example_rollouts.jsonl + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {num-gpus: "0", dashboard-host: "0.0.0.0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-head + env: + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + resources: *shared_head_resources + ports: + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + - {containerPort: 9090, name: gym-server} + volumeMounts: *dshm_mounts + volumes: *dshm_4 + + # ---------------- Training (GPU head + worker, generation colocated) ---------------- + training: + name: ${user:}-raycluster-gym-disagg-qwen3-4b + labels: + disagg.nemo-rl/cluster: gym-disagg-qwen3-4b + disagg.nemo-rl/run: qwen3-4b-gym-disagg + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-head + env: + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: + name: wandb-api-key + key: WANDB_API_KEY + resources: *shared_head_resources + ports: *gpu_head_ports + volumeMounts: *dshm_mounts + volumes: *dshm_16 + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: {num-gpus: "8", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-worker + env: *shared_worker_env + resources: *gpu_worker_resources + volumeMounts: *dshm_mounts + volumes: *dshm_64 diff --git a/infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml b/infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml new file mode 100644 index 00000000000..ad15c927faa --- /dev/null +++ b/infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml @@ -0,0 +1,49 @@ +# Qwen3-4B GRPO on instruction_following gym — **gym-disaggregated variant**. +# +# Differs from qwen3_4b_if.yaml in a single field: generation is colocated +# with training (both run inside the same Ray cluster's GPU workers), so we +# don't need a standalone generation RayCluster or its daemon. +# +# Pair with qwen3_4b_if_gym_disagg.infra.yaml (which declares only training + +# gym clusters — no generation cluster). +defaults: ../../../examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml + +cluster: + gpus_per_node: 8 + num_nodes: 1 +grpo: + num_prompts_per_step: 32 + num_generations_per_prompt: 16 + max_num_steps: 200 + val_period: 50 + max_rollout_turns: 1 + async_grpo: + # NeMo-RL asserts non-colocated when async_grpo is enabled. Single-cluster + # runs colocated generation, so we keep GRPO synchronous here. + enabled: false +policy: + train_global_batch_size: 256 + train_micro_batch_size: 1 + logprob_batch_size: 1 + # Colocated vLLM + backward on 4B takes too much GPU at 16K context; + # halving leaves headroom for vLLM's 6 GiB + activations + optimizer. + max_total_sequence_length: 8192 + dtensor_cfg: + activation_checkpointing: true + generation: + colocated: + enabled: true # recipe-level difference from disagg + resources: + gpus_per_node: 8 + num_nodes: 1 + vllm_cfg: + gpu_memory_utilization: 0.45 # leave 55% for training state +checkpointing: + save_period: 50 +logger: + wandb_enabled: true + wandb: + project: nemorl-gym-disagg-k8s + name: gym-disagg-if-qwen3-4b + tensorboard_enabled: true + monitor_gpus: true diff --git a/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml b/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml new file mode 100644 index 00000000000..7f23fabe063 --- /dev/null +++ b/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml @@ -0,0 +1,150 @@ +# Infra for qwen3_4b_if_single.yaml on a GB300 (p6e-gb300r.36xlarge) EKS cluster. +# +# Per-node hardware: 4× NVIDIA-GB300, ~140 CPU, ~925 GiB memory, arm64. GB300 +# nodes advertise no taints on this cluster — plain nodeSelector is enough. +# There is no aws-efa-k8s-device-plugin here, so vpc.amazonaws.com/efa is not +# a schedulable resource; NCCL/UCX ride the Mellanox NICs over hostNetwork. +# +# Sizing mirrors the paired recipe (1 node × 4 GPUs). Namespace is inferred +# from the current kube context (default — the FSx Lustre PVC `rl-workspace` +# is namespace-scoped and lives there, so experiments run in `default`). + +_shared: + nodeSelector: &shared_node_selector + nvidia.com/gpu.product: NVIDIA-GB300 + headEnv: &shared_head_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + workerEnv: &shared_worker_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + headResources: &shared_head_resources + limits: {cpu: "8", memory: "32Gi"} + requests: {cpu: "2", memory: "8Gi"} + gpuWorkerResources: &gpu_worker_resources + # p6e-gb300r.36xlarge allocatable: ~139.6 CPU, ~924 GiB, 4 GPUs. + # Claim 128 CPU / 880 GiB to leave headroom for the Ray head pod + + # gpu-operator / device-plugin / dgxc daemonsets (~11 CPU, ~40 GiB). + limits: + cpu: "128" + memory: "880Gi" + nvidia.com/gpu: "4" + requests: + cpu: "128" + memory: "880Gi" + nvidia.com/gpu: "4" + gpuHeadPorts: &gpu_head_ports + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + dshmMounts: &dshm_mounts + - {mountPath: /dev/shm, name: dshm} + dshm16: &dshm_16 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 16Gi} + dshm64: &dshm_64 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 64Gi} + +# namespace intentionally omitted — CLI auto-infers `default` from kube context. +image: nvcr.io/nvidian/nemo-rl:nightly +imagePullSecrets: [nvcr-secret] +serviceAccount: nemo-rl-endpoint-registry + +labels: + nrl-k8s/owner: ${user:} + +launch: + mode: attach + attach: + training: ${user:}-raycluster-single-qwen3-4b-gb300 + peerWatcher: false + entrypoint: | + pip install kubernetes -q || true + # Data files are pre-staged on the pods via kubectl cp. + export DISAGG_TRAIN_PATH=/tmp/train.jsonl + export DISAGG_VALID_PATH=/tmp/validation.jsonl + # NOTE: PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True is incompatible + # with colocated vLLM's CuMemAllocator (AssertionError at vLLM init). + export RAY_enable_infeasible_task_early_exit=true + export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM + python -u examples/nemo_gym/run_grpo_nemo_gym.py \ + --config nrl_k8s_run.yaml \ + ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ + ~policy.optimizer.kwargs.foreach \ + ~policy.optimizer.kwargs.fused + env: {} + rayUploadPaths: + - nemo_rl + - examples + - infra/examples + - tests/check_metrics.py + - tests/json_dump_tb_logs.py + - 3rdparty/Gym-workspace/Gym/nemo_gym + - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model + - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent + - 3rdparty/Gym-workspace/Gym/pyproject.toml + - 3rdparty/Gym-workspace/Gym/uv.lock + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/app.py + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/requirements.txt + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/tests + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/validation.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example_rollouts.jsonl + - 3rdparty/Megatron-LM-workspace/Megatron-LM/megatron + +clusters: + # ---------------- Training (GPU head + worker: generation + gym colocated) ---------------- + training: + name: ${user:}-raycluster-single-qwen3-4b-gb300 + labels: + disagg.nemo-rl/cluster: single-qwen3-4b-gb300 + disagg.nemo-rl/run: qwen3-4b-single-gb300 + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + containers: + - name: ray-head + env: + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: + name: wandb-api-key + key: WANDB_API_KEY + resources: *shared_head_resources + ports: *gpu_head_ports + volumeMounts: *dshm_mounts + volumes: *dshm_16 + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: {num-gpus: "4", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: *shared_node_selector + containers: + - name: ray-worker + env: *shared_worker_env + resources: *gpu_worker_resources + volumeMounts: *dshm_mounts + volumes: *dshm_64 diff --git a/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml b/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml new file mode 100644 index 00000000000..abb983c2b97 --- /dev/null +++ b/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml @@ -0,0 +1,181 @@ +# Prod-mode infra for qwen3_4b_if_single.yaml on GB300. +# +# Same topology as qwen3_4b_if_single.gb300.infra.yaml (1 node × 4 GPUs on +# p6e-gb300r.36xlarge), but submission is prod-shaped: +# +# submit.submitter portForward — Ray Job SDK via kubectl port-forward. +# launch.runMode batch — CLI returns after Ray accepts the job. +# launch.codeSource image — no working_dir upload; code already +# on the pod's filesystem. +# launch.codePath /opt/nemo-rl — the RL repo lives here inside every +# head + worker pod, served off the +# shared FSx Lustre PVC via subPath. +# +# Because /opt/nemo-rl is the Lustre checkout at ${user:}/nemo-rl, edits on Lustre +# show up in the next submission without rebuilding the image. That's the whole +# point of prod mode: iterate on code without churning the container. +# +# Prereqs (onboarding §3–§5 already applied in `default`): +# - rl-workspace PVC bound (shared FSx Lustre, 2.4 TiB, RWX) +# - /mnt/rl-workspace/${user:}/nemo-rl contains a git checkout of +# https://github.com/NVIDIA-NeMo/RL on branch hemil/k8s-infra-cp +# - nvcr-secret, wandb-api-key, nemo-rl-endpoint-registry all present +# +# Usage: +# # Long-lived cluster — idempotent; reuses the live cluster if it +# # already matches, applies if absent. Submits training per invocation. +# nrl-k8s run tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml \ +# --run-id single-if-$(date +%Y%m%d-%H%M%S) +# +# # One-shot with auto-teardown (ephemeral RayCluster): +# nrl-k8s rayjob tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ +# --infra tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml + +_shared: + nodeSelector: &shared_node_selector + nvidia.com/gpu.product: NVIDIA-GB300 + headEnv: &shared_head_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + workerEnv: &shared_worker_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + headResources: &shared_head_resources + limits: {cpu: "8", memory: "32Gi"} + requests: {cpu: "2", memory: "8Gi"} + gpuWorkerResources: &gpu_worker_resources + # p6e-gb300r.36xlarge allocatable: ~139.6 CPU / ~924 GiB / 4 GPUs. + limits: + cpu: "128" + memory: "880Gi" + nvidia.com/gpu: "4" + requests: + cpu: "128" + memory: "880Gi" + nvidia.com/gpu: "4" + gpuHeadPorts: &gpu_head_ports + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + # Volumes. The Lustre PVC is mounted at /opt/nemo-rl (subPath ${user:}/nemo-rl) + # for code, and at /mnt/rl-workspace (no subPath) for run outputs, caches, + # and checkpoints. dshm stays as an emptyDir for Ray's object store. + codeMounts: &code_mounts + - {mountPath: /dev/shm, name: dshm} + - {mountPath: /opt/nemo-rl, name: rl-workspace, subPath: "${user:}/nemo-rl"} + - {mountPath: /mnt/rl-workspace, name: rl-workspace} + headVolumes: &head_volumes + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 16Gi} + - name: rl-workspace + persistentVolumeClaim: {claimName: rl-workspace} + workerVolumes: &worker_volumes + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 64Gi} + - name: rl-workspace + persistentVolumeClaim: {claimName: rl-workspace} + +# namespace auto-inferred from kube context (default). +image: nvcr.io/nvidian/nemo-rl:nightly +imagePullSecrets: [nvcr-secret] +serviceAccount: nemo-rl-endpoint-registry + +labels: + nrl-k8s/owner: ${user:} + +submit: + submitter: portForward + +launch: + mode: attach + runMode: batch + codeSource: image + codePath: /opt/nemo-rl + attach: + training: ${user:}-raycluster-single-qwen3-4b-gb300-prod + peerWatcher: false + env: {} + # Runs inside the training head pod under Ray's Job SDK. cwd inherits + # from the pod's default, so `cd /opt/nemo-rl` is explicit. The config + # path is repo-relative — load_config follows the recipe's `defaults:` + # up to grpo_qwen3_4b_instruct_k8s_base.yaml automatically. + entrypoint: | + set -eu + cd /opt/nemo-rl + if [ ! -d /mnt/rl-workspace ]; then + echo "ERROR: /mnt/rl-workspace not mounted — is the rl-workspace PVC bound?" >&2 + exit 1 + fi + # Full instruction-following dataset (~20K prompts) snapshotted from + # huggingface.co/datasets/nvidia/Nemotron-RL-instruction_following onto the + # shared Lustre workspace. + IF_DATA=/mnt/rl-workspace/${user:}/datasets/instruction_following/instruction_following.jsonl + export DISAGG_TRAIN_PATH=$IF_DATA + export DISAGG_VALID_PATH=$IF_DATA + export RAY_enable_infeasible_task_early_exit=true + export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM + LOG_DIR=/mnt/rl-workspace/${user:}/driver_logs + LOG=\${LOG_DIR}/${user:}-raycluster-single-qwen3-4b-gb300-prod-training-$(date -u +%Y%m%d-%H%M%S-%N).log + mkdir -p "\${LOG_DIR}" + + python -u examples/nemo_gym/run_grpo_nemo_gym.py \ + --config tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ + ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ + ~policy.optimizer.kwargs.foreach \ + ~policy.optimizer.kwargs.fused \ + 2>&1 | tee "\${LOG}" + +clusters: + training: + name: ${user:}-raycluster-single-qwen3-4b-gb300-prod + labels: + disagg.nemo-rl/cluster: single-qwen3-4b-gb300-prod + disagg.nemo-rl/run: qwen3-4b-single-gb300-prod + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + containers: + - name: ray-head + env: + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: + name: wandb-api-key + key: WANDB_API_KEY + resources: *shared_head_resources + ports: *gpu_head_ports + volumeMounts: *code_mounts + volumes: *head_volumes + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: {num-gpus: "4", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: *shared_node_selector + containers: + - name: ray-worker + env: *shared_worker_env + resources: *gpu_worker_resources + volumeMounts: *code_mounts + volumes: *worker_volumes diff --git a/infra/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml b/infra/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml new file mode 100644 index 00000000000..de51edf3722 --- /dev/null +++ b/infra/nrl_k8s/examples/qwen3_4b_if_single.infra.yaml @@ -0,0 +1,169 @@ +# Infra for single-cluster Qwen3-4B colocated GRPO run. +# +# A single RayCluster hosting training + colocated vLLM generation + a local +# Gym Ray actor. No separate gym or generation cluster — that's the whole +# point of single-cluster: the training entrypoint calls +# create_env(env_name="nemo_gym", ...) +# which pins the Gym actor to a non-head worker via NodeAffinityScheduling, +# so we must size the GPU worker big enough to host *both* the vLLM engines +# (8 GPU) and the Gym actor (~4 CPU). Namespace is auto-inferred from the +# kube context (nemo-rl-testing). + +_shared: + nodeSelector: &shared_node_selector + gpu-wrangler.nvidia.com/lease: nemo-rl-testing + tolerations: &shared_tolerations + - {key: gpu-wrangler.nvidia.com/lease, operator: Equal, value: nemo-rl-testing, effect: NoSchedule} + - {key: platform.nvidia.com/gpu, operator: Equal, value: "true", effect: NoSchedule} + # Image ships aws-ofi + enp71s0 defaults; keep pod env minimal. + headEnv: &shared_head_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + workerEnv: &shared_worker_env + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: GLOO_SOCKET_IFNAME, value: enp71s0} + - {name: NCCL_SOCKET_IFNAME, value: enp71s0} + - {name: FI_PROVIDER, value: efa} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + headResources: &shared_head_resources + limits: {cpu: "8", memory: "32Gi"} + requests: {cpu: "2", memory: "8Gi"} + gpuWorkerResources: &gpu_worker_resources + # Sized for the full p5.48xlarge (192 CPU / ~1957 GiB allocatable). + # Single-cluster needs the worker to host training + colocated vLLM + + # the Gym actor (~4 CPU). We claim 184 CPU / 1850 GiB (leaving ~8 CPU + + # ~100 GiB for the Ray head pod + k8s daemonsets: kube-proxy, aws-node, + # nvidia-device-plugin, node-exporter). 32 EFA devices = full NIC set. + limits: + cpu: "184" + memory: "1850Gi" + nvidia.com/gpu: "8" + vpc.amazonaws.com/efa: "32" + requests: + cpu: "184" + memory: "1850Gi" + nvidia.com/gpu: "8" + vpc.amazonaws.com/efa: "32" + gpuHeadPorts: &gpu_head_ports + - {containerPort: 6379, name: gcs-server} + - {containerPort: 8265, name: dashboard} + - {containerPort: 10001, name: client} + dshmMounts: &dshm_mounts + - {mountPath: /dev/shm, name: dshm} + dshm16: &dshm_16 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 16Gi} + dshm64: &dshm_64 + - name: dshm + emptyDir: {medium: Memory, sizeLimit: 64Gi} + +# namespace intentionally omitted — CLI auto-infers nemo-rl-testing from +# the current kube context. +image: nvcr.io/nvidian/nemo-rl:nightly +imagePullSecrets: [nvidia-ngcuser-pull-secret, ngc-registry-secret] +serviceAccount: nemo-rl-endpoint-registry + +labels: + nrl-k8s/owner: ${user:} + +launch: + mode: attach + attach: + training: ${user:}-raycluster-single-qwen3-4b + peerWatcher: false + # Single-cluster: no remote_generation_url (colocated), no disagg_job_id + # (no endpoint registry — gym is a local Ray actor). + entrypoint: | + pip install kubernetes -q || true + export GLOO_SOCKET_IFNAME=enp71s0 + export NCCL_SOCKET_IFNAME=enp71s0 + export FI_PROVIDER=efa + # Data files are pre-staged on the pods via kubectl cp. + export DISAGG_TRAIN_PATH=/tmp/train.jsonl + export DISAGG_VALID_PATH=/tmp/validation.jsonl + # NOTE: PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True is incompatible + # with colocated vLLM's CuMemAllocator (AssertionError at vLLM init). + export RAY_enable_infeasible_task_early_exit=true + export PYTHONPATH=.:3rdparty/Gym-workspace/Gym:3rdparty/Megatron-LM-workspace/Megatron-LM + python -u examples/nemo_gym/run_grpo_nemo_gym.py \ + --config nrl_k8s_run.yaml \ + ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ + ~policy.optimizer.kwargs.foreach \ + ~policy.optimizer.kwargs.fused + env: {} + rayUploadPaths: + - nemo_rl + - examples + - infra/examples + - tests/check_metrics.py + - tests/json_dump_tb_logs.py + - 3rdparty/Gym-workspace/Gym/nemo_gym + - 3rdparty/Gym-workspace/Gym/responses_api_models/vllm_model + - 3rdparty/Gym-workspace/Gym/responses_api_agents/simple_agent + - 3rdparty/Gym-workspace/Gym/pyproject.toml + - 3rdparty/Gym-workspace/Gym/uv.lock + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/app.py + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/requirements.txt + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/configs + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/tests + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/validation.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example.jsonl + - 3rdparty/Gym-workspace/Gym/resources_servers/instruction_following/data/example_rollouts.jsonl + - 3rdparty/Megatron-LM-workspace/Megatron-LM/megatron + +clusters: + # ---------------- Training (GPU head + worker: generation + gym colocated) ---------------- + training: + name: ${user:}-raycluster-single-qwen3-4b + labels: + disagg.nemo-rl/cluster: single-qwen3-4b + disagg.nemo-rl/run: qwen3-4b-single + spec: + rayVersion: "2.52.0" + headGroupSpec: + rayStartParams: {dashboard-host: "0.0.0.0", num-gpus: "0", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + dnsPolicy: ClusterFirst + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-head + env: + - {name: HF_HOME, value: /tmp/huggingface} + - {name: TRANSFORMERS_CACHE, value: /tmp/huggingface} + - {name: NCCL_DEBUG, value: INFO} + - {name: RAY_memory_monitor_refresh_ms, value: "0"} + - name: WANDB_API_KEY + valueFrom: + secretKeyRef: + name: wandb-api-key + key: WANDB_API_KEY + resources: *shared_head_resources + ports: *gpu_head_ports + volumeMounts: *dshm_mounts + volumes: *dshm_16 + workerGroupSpecs: + - groupName: gpu-workers + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + rayStartParams: {num-gpus: "8", object-store-memory: "200000000"} + template: + spec: + schedulerName: default-scheduler + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: *shared_node_selector + tolerations: *shared_tolerations + containers: + - name: ray-worker + env: *shared_worker_env + resources: *gpu_worker_resources + volumeMounts: *dshm_mounts + volumes: *dshm_64 diff --git a/infra/nrl_k8s/examples/qwen3_4b_if_single.yaml b/infra/nrl_k8s/examples/qwen3_4b_if_single.yaml new file mode 100644 index 00000000000..9ec4dd473af --- /dev/null +++ b/infra/nrl_k8s/examples/qwen3_4b_if_single.yaml @@ -0,0 +1,50 @@ +# Qwen3-4B GRPO on instruction_following gym — **single RayCluster variant**. +# +# Training, colocated vLLM generation, AND gym all run inside a single +# RayCluster. Gym is spawned as a local Ray actor by +# examples/nemo_gym/run_grpo_nemo_gym.py (create_env(env_name="nemo_gym", ...)) +# because neither env.disagg_job_id nor env.remote_gym_url is set. +# +# Pair with qwen3_4b_if_single.infra.yaml (declares only the training cluster; +# no separate gym or generation cluster). +defaults: ../../../examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml + +cluster: + gpus_per_node: 4 + num_nodes: 1 +grpo: + num_prompts_per_step: 32 + num_generations_per_prompt: 16 + max_num_steps: 500 + val_period: 50 + max_rollout_turns: 1 + async_grpo: + # NeMo-RL asserts non-colocated when async_grpo is enabled. Single-cluster runs + # colocated generation, so GRPO must remain synchronous. + enabled: false +policy: + train_global_batch_size: 256 + train_micro_batch_size: 1 + logprob_batch_size: 1 + # Colocated vLLM + backward on 4B takes too much GPU at 16K context; + # halving leaves headroom for vLLM's KV cache + activations + optimizer. + max_total_sequence_length: 8192 + dtensor_cfg: + activation_checkpointing: true + generation: + colocated: + enabled: true + resources: + gpus_per_node: 4 + num_nodes: 1 + vllm_cfg: + gpu_memory_utilization: 0.45 # leave 55% for training state +checkpointing: + save_period: 50 +logger: + wandb_enabled: true + wandb: + project: nemorl-single-k8s + name: single-if-qwen3-4b + tensorboard_enabled: true + monitor_gpus: true From a02a9e9fcb67b0e31974123fe9006d643c0b29c4 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 19:31:27 -0700 Subject: [PATCH 74/84] feat: NemoGym disaggregated mode with standalone Gym server (disagg part) Signed-off-by: Terry Kong --- 3rdparty/Gym-workspace/Gym | 2 +- examples/nemo_gym/run_grpo_nemo_gym.py | 23 ++++++++++++ nemo_rl/environments/nemo_gym.py | 50 +++++++++++++++++++++----- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/3rdparty/Gym-workspace/Gym b/3rdparty/Gym-workspace/Gym index 1a4912e231b..01a9765f8cb 160000 --- a/3rdparty/Gym-workspace/Gym +++ b/3rdparty/Gym-workspace/Gym @@ -1 +1 @@ -Subproject commit 1a4912e231bb2795b062f7de97496caaf382c7f6 +Subproject commit 01a9765f8cba758c12a018b0e0e4d861ee4b916c diff --git a/examples/nemo_gym/run_grpo_nemo_gym.py b/examples/nemo_gym/run_grpo_nemo_gym.py index 34b6f0b5db7..9ee0cc32386 100644 --- a/examples/nemo_gym/run_grpo_nemo_gym.py +++ b/examples/nemo_gym/run_grpo_nemo_gym.py @@ -212,6 +212,29 @@ def main() -> None: base_urls=policy_generation.dp_openai_server_base_urls, initial_global_config_dict=config["env"]["nemo_gym"], ) + # Support disaggregated Gym: connect to a remote Gym service instead of spawning local subprocesses. + # Two modes: (1) static URL via env.remote_gym_url, or (2) K8s endpoint registry via env.disagg_job_id. + remote_gym_url = config["env"].get("remote_gym_url") + disagg_job_id = config["env"].get("disagg_job_id") + if disagg_job_id: + import json + + from nemo_rl.distributed.k8s_endpoint_registry import K8sEndpointRegistry + + registry = K8sEndpointRegistry(job_id=disagg_job_id) + registry.create(owner_raycluster_name=os.environ.get("RAY_CLUSTER_NAME")) + + # Publish vLLM URLs so the Gym cluster can discover them. + vllm_urls = [u for u in policy_generation.dp_openai_server_base_urls if u] + registry.set("vllm_base_urls", json.dumps(vllm_urls)) + + # Wait for the Gym cluster to register its head server address. + print("Waiting for Gym head server to register in endpoint registry...") + remote_gym_url = registry.get("gym_head_server") + print(f"Discovered remote Gym service at: {remote_gym_url}") + if remote_gym_url: + nemo_gym_config["remote_gym_url"] = remote_gym_url + print(f"Using remote Gym service at: {remote_gym_url}") nemo_gym = create_env(env_name="nemo_gym", env_config=nemo_gym_config) # Blocking wait for NeMo-Gym to spin up ray.get(nemo_gym.health_check.remote()) diff --git a/nemo_rl/environments/nemo_gym.py b/nemo_rl/environments/nemo_gym.py index b32f76919ff..aa4200f4c39 100644 --- a/nemo_rl/environments/nemo_gym.py +++ b/nemo_rl/environments/nemo_gym.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. from pathlib import Path -from typing import Any, Dict, List, TypedDict +from typing import Any, Dict, List, NotRequired, TypedDict import ray import torch @@ -27,6 +27,9 @@ class NemoGymConfig(TypedDict): model_name: str base_urls: List[str] initial_global_config_dict: Dict[str, Any] + remote_gym_url: NotRequired[ + str + ] # If set, connects to a remote Gym service instead of spawning local subprocesses @ray.remote(max_restarts=-1, max_task_retries=-1) # pragma: no cover @@ -35,27 +38,55 @@ class NemoGym(EnvironmentInterface): def __init__(self, cfg: NemoGymConfig): self.cfg = cfg + self._remote_mode = bool(cfg.get("remote_gym_url")) - self.node_ip = _get_node_ip_local() - self.head_server_port = _get_free_port_local() + from nemo_gym.rollout_collection import RolloutCollectionHelper + from nemo_gym.server_utils import BaseServerConfig + + if self._remote_mode: + # Remote mode: connect to an external Gym HTTP service. + # No local subprocesses are spawned. + remote_url = cfg["remote_gym_url"] + # Parse host:port from URL like "http://gym-service:8080" or "gym-service:8080" + url = remote_url.removeprefix("http://").removeprefix("https://") + if ":" in url: + host, port_str = url.rsplit(":", 1) + port = int(port_str.rstrip("/")) + else: + host, port = url.rstrip("/"), 8080 + print(f"NemoGym remote mode: connecting to {host}:{port}") + + self.rh = None + self.head_server_config = BaseServerConfig(host=host, port=port) + self.rch = RolloutCollectionHelper() + initial_global_config_dict = cfg.get("initial_global_config_dict") or {} + self.rollout_max_attempts_to_avoid_lp_nan = initial_global_config_dict.pop( + "rollout_max_attempts_to_avoid_lp_nan", 1 + ) + else: + # Colocated mode: spawn Gym subprocesses locally (original behavior). + self._init_colocated(cfg) + + def _init_colocated(self, cfg: NemoGymConfig): from nemo_gym.cli import GlobalConfigDictParserConfig, RunHelper from nemo_gym.rollout_collection import RolloutCollectionHelper from nemo_gym.server_utils import HEAD_SERVER_KEY_NAME, BaseServerConfig from omegaconf import DictConfig + self.node_ip = _get_node_ip_local() + self.head_server_port = _get_free_port_local() + RELATIVE_PATH = "nemo_rl/environments/nemo_gym.py" assert __file__.endswith(RELATIVE_PATH) - initial_global_config_dict = ( - self.cfg.get("initial_global_config_dict") or dict() - ) + initial_global_config_dict = cfg.get("initial_global_config_dict") or dict() # Policy information - initial_global_config_dict["policy_model_name"] = self.cfg["model_name"] + initial_global_config_dict["policy_model_name"] = cfg["model_name"] initial_global_config_dict["policy_api_key"] = ( "dummy_key" # No key necessary for training. ) - initial_global_config_dict["policy_base_url"] = self.cfg["base_urls"] + initial_global_config_dict["policy_base_url"] = cfg["base_urls"] initial_global_config_dict.setdefault( "global_aiohttp_connector_limit_per_host", 16_384 @@ -253,7 +284,8 @@ def _postprocess_nemo_gym_to_nemo_rl_result( } def shutdown(self) -> None: - self.rh.shutdown() + if self.rh is not None: + self.rh.shutdown() def step(self, message_log_batch, metadata): # This is not used since NeMo-Gym will handle the rollouts entirely. From 64ce4c782dbea50ef331a6e6063541b7fdefc076 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 19:31:27 -0700 Subject: [PATCH 75/84] infra: K8s ConfigMap endpoint registry for disaggregated service discovery (disagg part) Signed-off-by: Terry Kong --- nemo_rl/distributed/k8s_endpoint_registry.py | 225 +++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 nemo_rl/distributed/k8s_endpoint_registry.py diff --git a/nemo_rl/distributed/k8s_endpoint_registry.py b/nemo_rl/distributed/k8s_endpoint_registry.py new file mode 100644 index 00000000000..d2e64979120 --- /dev/null +++ b/nemo_rl/distributed/k8s_endpoint_registry.py @@ -0,0 +1,225 @@ +# 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. +"""ConfigMap-backed endpoint registry for disaggregated RL-Gym service discovery. + +Each (RL, Gym) job pair shares a ConfigMap named 'nemo-rl-endpoints-{job_id}'. +Both sides read/write their dynamic addresses (IP:port) to it. The ConfigMap +has an ownerReference to the RL RayCluster so it's garbage collected on teardown. + +Usage (RL side): + registry = K8sEndpointRegistry(job_id="my-job") + registry.create(owner_raycluster_name="raycluster-rl") + registry.set("vllm_base_urls", json.dumps(["http://10.0.0.1:8000/v1"])) + gym_url = registry.get("gym_head_server") # blocks until Gym registers + +Usage (Gym side): + registry = K8sEndpointRegistry(job_id="my-job") + registry.set("gym_head_server", "http://10.0.0.2:8080") + vllm_urls = json.loads(registry.get("vllm_base_urls")) # blocks until RL registers +""" + +from __future__ import annotations + +import time +from pathlib import Path + +from kubernetes import client, config +from kubernetes.client.exceptions import ApiException + +CONFIGMAP_PREFIX = "nemo-rl-endpoints" + + +class K8sEndpointRegistry: + """Shared endpoint registry backed by a K8s ConfigMap.""" + + def __init__(self, job_id: str, namespace: str | None = None): + self.job_id = job_id + self.configmap_name = f"{CONFIGMAP_PREFIX}-{job_id}" + + # Auto-detect namespace from in-pod mount, or fall back to "default". + if namespace is not None: + self.namespace = namespace + else: + ns_path = Path("/var/run/secrets/kubernetes.io/serviceaccount/namespace") + self.namespace = ( + ns_path.read_text().strip() if ns_path.exists() else "default" + ) + + # Load K8s client config — in-cluster when running in a pod, kubeconfig otherwise. + try: + config.load_incluster_config() + except config.ConfigException: + config.load_kube_config() + + self._v1 = client.CoreV1Api() + self._custom = client.CustomObjectsApi() + + def create(self, owner_raycluster_name: str | None = None) -> None: + """Create the ConfigMap. Idempotent — no-op if it already exists. + + Args: + owner_raycluster_name: If set, the ConfigMap gets an ownerReference + to this RayCluster so K8s garbage-collects it on teardown. + """ + owner_references = None + if owner_raycluster_name: + owner_references = self._build_owner_reference(owner_raycluster_name) + + cm = client.V1ConfigMap( + metadata=client.V1ObjectMeta( + name=self.configmap_name, + namespace=self.namespace, + owner_references=owner_references, + ), + data={}, + ) + + try: + self._v1.create_namespaced_config_map(namespace=self.namespace, body=cm) + print(f"Created endpoint registry ConfigMap: {self.configmap_name}") + except ApiException as e: + if e.status == 409: + # Already exists — patch ownerReferences if we have them + # (handles race where Gym's set() created it before RL's create()). + if owner_references: + self._v1.patch_namespaced_config_map( + name=self.configmap_name, + namespace=self.namespace, + body=client.V1ConfigMap( + metadata=client.V1ObjectMeta( + owner_references=owner_references, + ) + ), + ) + print( + f"Patched ownerReference on existing ConfigMap: {self.configmap_name}" + ) + else: + print( + f"Endpoint registry ConfigMap already exists: {self.configmap_name}" + ) + else: + raise + + def set(self, key: str, value: str) -> None: + """Write a key-value pair to the ConfigMap. Creates the ConfigMap if needed.""" + try: + cm = self._v1.read_namespaced_config_map( + name=self.configmap_name, namespace=self.namespace + ) + if cm.data is None: + cm.data = {} + cm.data[key] = value + self._v1.patch_namespaced_config_map( + name=self.configmap_name, namespace=self.namespace, body=cm + ) + except ApiException as e: + if e.status == 404: + # ConfigMap doesn't exist yet — create it with this key. + try: + cm = client.V1ConfigMap( + metadata=client.V1ObjectMeta( + name=self.configmap_name, namespace=self.namespace + ), + data={key: value}, + ) + self._v1.create_namespaced_config_map( + namespace=self.namespace, body=cm + ) + except ApiException as create_err: + if create_err.status == 409: + # Another process created it between our read and create — retry patch. + self.set(key, value) + return + raise + else: + raise + print(f"Registered endpoint: {key} = {value}") + + def get(self, key: str, timeout: float = 600, poll_interval: float = 2) -> str: + """Poll until a key appears in the ConfigMap, then return its value. + + Args: + key: The key to wait for. + timeout: Max seconds to wait before raising TimeoutError. + poll_interval: Seconds between polls. + """ + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + value = self.get_nowait(key) + if value is not None: + return value + remaining = deadline - time.monotonic() + if remaining <= 0: + break + time.sleep(min(poll_interval, remaining)) + raise TimeoutError( + f"Timed out after {timeout}s waiting for key '{key}' " + f"in ConfigMap '{self.configmap_name}'" + ) + + def get_nowait(self, key: str) -> str | None: + """Non-blocking read. Returns None if key or ConfigMap doesn't exist.""" + try: + cm = self._v1.read_namespaced_config_map( + name=self.configmap_name, namespace=self.namespace + ) + if cm.data is None: + return None + return cm.data.get(key) + except ApiException as e: + if e.status == 404: + return None + raise + + def signal_error(self, message: str) -> None: + """Write an error to the ConfigMap. + + The peer-watcher sidecar monitors this and triggers teardown + when it sees a non-empty 'error' key. + """ + self.set("error", message) + + def _build_owner_reference( + self, raycluster_name: str + ) -> list[client.V1OwnerReference]: + """Look up the RayCluster's UID and build an ownerReference.""" + try: + rc = self._custom.get_namespaced_custom_object( + group="ray.io", + version="v1", + namespace=self.namespace, + plural="rayclusters", + name=raycluster_name, + ) + uid = rc["metadata"]["uid"] + except ApiException as e: + if e.status == 404: + print( + f"Warning: RayCluster '{raycluster_name}' not found " + f"for ownerReference — ConfigMap will not be auto-cleaned." + ) + return None + raise + + return [ + client.V1OwnerReference( + api_version="ray.io/v1", + kind="RayCluster", + name=raycluster_name, + uid=uid, + controller=True, + block_owner_deletion=False, + ) + ] From 990453d37a66b781366f6457f61cecdbe9feab4e Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 19:31:27 -0700 Subject: [PATCH 76/84] infra: move standalone gym server from Gym submodule to nemo-rl (disagg part) Signed-off-by: Terry Kong --- nemo_rl/distributed/standalone_gym_server.py | 192 +++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 nemo_rl/distributed/standalone_gym_server.py diff --git a/nemo_rl/distributed/standalone_gym_server.py b/nemo_rl/distributed/standalone_gym_server.py new file mode 100644 index 00000000000..650ba48ac41 --- /dev/null +++ b/nemo_rl/distributed/standalone_gym_server.py @@ -0,0 +1,192 @@ +# 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. +"""Standalone NeMo Gym server for disaggregated RL-Gym deployments. + +Starts the Gym servers (head, agent, model, resource) as a long-running +HTTP service that can be deployed on a separate Kubernetes cluster/pod. + +Service discovery uses a shared K8s ConfigMap (endpoint registry). The Gym +server registers its address and waits for the RL cluster to register vLLM URLs. + +The --config-yaml should contain the env.nemo_gym section from the GRPO config, +which includes config_paths and server overrides. Paths in config_paths are +resolved relative to the Gym repo root (PARENT_DIR). + +Usage: + uv run --extra nemo_gym python -m nemo_rl.distributed.standalone_gym_server \ + --job-id my-job \ + --port 9090 \ + --model-name Qwen/Qwen3-0.6B \ + --config-yaml /path/to/gym_config.yaml + + # Or with static vLLM URLs (skips registry for vLLM discovery): + uv run --extra nemo_gym python -m nemo_rl.distributed.standalone_gym_server \ + --port 9090 \ + --model-name Qwen/Qwen3-0.6B \ + --vllm-base-urls http://10.0.0.1:8000/v1 \ + --config-yaml /path/to/gym_config.yaml +""" + +import argparse +import json +import signal +import sys +from pathlib import Path + +from nemo_gym.cli import GlobalConfigDictParserConfig, RunHelper +from nemo_gym.server_utils import HEAD_SERVER_KEY_NAME +from omegaconf import DictConfig, OmegaConf + + +def _get_node_ip() -> str: + import socket + + return socket.gethostbyname(socket.gethostname()) + + +def main(): + parser = argparse.ArgumentParser(description="Standalone NeMo Gym server") + parser.add_argument("--port", type=int, default=9090, help="Head server port") + parser.add_argument( + "--job-id", + type=str, + default=None, + help="Job ID for K8s ConfigMap endpoint registry. " + "If set, registers this server's address and discovers vLLM URLs via the registry.", + ) + parser.add_argument( + "--vllm-base-urls", + type=str, + nargs="+", + default=None, + help="vLLM HTTP server base URLs. If not set and --job-id is provided, " + "URLs are discovered via the K8s endpoint registry.", + ) + parser.add_argument( + "--model-name", + type=str, + required=True, + help="Policy model name (e.g., Qwen/Qwen3-0.6B)", + ) + parser.add_argument( + "--config-yaml", + type=str, + default=None, + help="Path to a YAML with the env.nemo_gym config section (config_paths, server overrides).", + ) + parser.add_argument( + "--dotenv-path", + type=str, + default=None, + help="Path to the env.yaml dotenv file", + ) + args = parser.parse_args() + + # Resolve vLLM URLs — either from CLI or from K8s endpoint registry. + vllm_base_urls = args.vllm_base_urls + if vllm_base_urls is None and args.job_id: + from nemo_rl.distributed.k8s_endpoint_registry import K8sEndpointRegistry + + registry = K8sEndpointRegistry(job_id=args.job_id) + + # Register our address first so RL can find us. + node_ip = _get_node_ip() + gym_url = f"http://{node_ip}:{args.port}" + registry.set("gym_head_server", gym_url) + print(f"Registered gym_head_server = {gym_url}") + + # Wait for RL to register vLLM URLs. + print("Waiting for vLLM base URLs from RL cluster...") + vllm_base_urls = json.loads(registry.get("vllm_base_urls")) + print(f"Discovered vLLM base URLs: {vllm_base_urls}") + elif vllm_base_urls is None: + print("Error: --vllm-base-urls or --job-id is required", file=sys.stderr) + sys.exit(1) + + # Build the global config dict from the config YAML. + # We use resolve=False to avoid failing on unresolvable interpolations + # (e.g., ${cluster.num_nodes}) that are only valid in the full GRPO config. + initial_global_config_dict = {} + if args.config_yaml: + loaded = OmegaConf.load(args.config_yaml) + initial_global_config_dict = OmegaConf.to_container(loaded, resolve=False) + + def _strip_interpolations(d): + if isinstance(d, dict): + return { + k: _strip_interpolations(v) + for k, v in d.items() + if not (isinstance(v, str) and "${" in v) + } + if isinstance(d, list): + return [_strip_interpolations(item) for item in d] + return d + + initial_global_config_dict = _strip_interpolations(initial_global_config_dict) + + # Remove RL-specific keys that don't apply to standalone mode. + initial_global_config_dict.pop("is_trajectory_collection", None) + initial_global_config_dict.pop("rollout_max_attempts_to_avoid_lp_nan", None) + + # Bind to actual pod IP instead of localhost so remote RL cluster can reach us. + initial_global_config_dict["use_absolute_ip"] = True + + # Set policy/connection config. + initial_global_config_dict["policy_model_name"] = args.model_name + initial_global_config_dict["policy_api_key"] = "dummy_key" + initial_global_config_dict["policy_base_url"] = vllm_base_urls + initial_global_config_dict.setdefault( + "global_aiohttp_connector_limit_per_host", 16_384 + ) + initial_global_config_dict.setdefault("global_aiohttp_connector_limit", 65_536) + + initial_global_config_dict[HEAD_SERVER_KEY_NAME] = { + "host": "0.0.0.0", + "port": args.port, + } + + # Determine dotenv path. + dotenv_path = Path(args.dotenv_path) if args.dotenv_path else None + + print(f"Starting standalone NeMo Gym server on port {args.port}") + print(f"vLLM base URLs: {vllm_base_urls}") + print(f"Model: {args.model_name}") + if "config_paths" in initial_global_config_dict: + print(f"Config paths: {initial_global_config_dict['config_paths']}") + + rh = RunHelper() + rh.start( + global_config_dict_parser_config=GlobalConfigDictParserConfig( + dotenv_path=dotenv_path, + initial_global_config_dict=DictConfig(initial_global_config_dict), + skip_load_from_cli=True, + ) + ) + + rh.display_server_instance_info() + print(f"\nStandalone Gym server is running on port {args.port}") + print("Press Ctrl+C to stop.") + + def handle_signal(signum, frame): + print(f"\nReceived signal {signum}, shutting down...") + rh.shutdown() + sys.exit(0) + + signal.signal(signal.SIGTERM, handle_signal) + signal.signal(signal.SIGINT, handle_signal) + signal.pause() + + +if __name__ == "__main__": + main() From e9f1de2f466159d35f620be3f81f082419cec828 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 19:31:27 -0700 Subject: [PATCH 77/84] address PR #2238 review feedback (disagg part) Signed-off-by: Terry Kong --- examples/nemo_gym/run_grpo_nemo_gym.py | 9 +- nemo_rl/distributed/k8s_endpoint_registry.py | 76 +- nemo_rl/distributed/standalone_gym_server.py | 19 +- nemo_rl/environments/nemo_gym.py | 20 +- pyproject.toml | 1 + uv.lock | 3335 ++++++++++-------- 6 files changed, 1976 insertions(+), 1484 deletions(-) diff --git a/examples/nemo_gym/run_grpo_nemo_gym.py b/examples/nemo_gym/run_grpo_nemo_gym.py index 9ee0cc32386..186b4cbf3b0 100644 --- a/examples/nemo_gym/run_grpo_nemo_gym.py +++ b/examples/nemo_gym/run_grpo_nemo_gym.py @@ -230,7 +230,14 @@ def main() -> None: # Wait for the Gym cluster to register its head server address. print("Waiting for Gym head server to register in endpoint registry...") - remote_gym_url = registry.get("gym_head_server") + try: + remote_gym_url = registry.get("gym_head_server") + except TimeoutError as e: + raise TimeoutError( + f"Timed out waiting for the Gym cluster to register its head server. " + f"Ensure the Gym RayCluster is running and the standalone_gym_server " + f"has started with --job-id={disagg_job_id}. Original error: {e}" + ) from e print(f"Discovered remote Gym service at: {remote_gym_url}") if remote_gym_url: nemo_gym_config["remote_gym_url"] = remote_gym_url diff --git a/nemo_rl/distributed/k8s_endpoint_registry.py b/nemo_rl/distributed/k8s_endpoint_registry.py index d2e64979120..fd15803712e 100644 --- a/nemo_rl/distributed/k8s_endpoint_registry.py +++ b/nemo_rl/distributed/k8s_endpoint_registry.py @@ -112,40 +112,54 @@ def create(self, owner_raycluster_name: str | None = None) -> None: else: raise - def set(self, key: str, value: str) -> None: + def set(self, key: str, value: str, _max_retries: int = 5) -> None: """Write a key-value pair to the ConfigMap. Creates the ConfigMap if needed.""" - try: - cm = self._v1.read_namespaced_config_map( - name=self.configmap_name, namespace=self.namespace - ) - if cm.data is None: - cm.data = {} - cm.data[key] = value - self._v1.patch_namespaced_config_map( - name=self.configmap_name, namespace=self.namespace, body=cm - ) - except ApiException as e: - if e.status == 404: - # ConfigMap doesn't exist yet — create it with this key. - try: - cm = client.V1ConfigMap( - metadata=client.V1ObjectMeta( - name=self.configmap_name, namespace=self.namespace - ), - data={key: value}, - ) - self._v1.create_namespaced_config_map( - namespace=self.namespace, body=cm + for attempt in range(_max_retries): + try: + cm = self._v1.read_namespaced_config_map( + name=self.configmap_name, namespace=self.namespace + ) + if cm.data is None: + cm.data = {} + cm.data[key] = value + self._v1.patch_namespaced_config_map( + name=self.configmap_name, namespace=self.namespace, body=cm + ) + except ApiException as e: + if e.status == 409: + # Conflict on patch — another writer updated concurrently. Retry. + print( + f"Conflict on patch (attempt {attempt + 1}/{_max_retries}), retrying..." ) - except ApiException as create_err: - if create_err.status == 409: - # Another process created it between our read and create — retry patch. - self.set(key, value) - return + continue + if e.status == 404: + # ConfigMap doesn't exist yet — create it with this key. + try: + cm = client.V1ConfigMap( + metadata=client.V1ObjectMeta( + name=self.configmap_name, namespace=self.namespace + ), + data={key: value}, + ) + self._v1.create_namespaced_config_map( + namespace=self.namespace, body=cm + ) + except ApiException as create_err: + if create_err.status == 409: + # Another process created it between our read and create — retry. + print( + f"Conflict on create (attempt {attempt + 1}/{_max_retries}), retrying..." + ) + continue + raise + else: raise - else: - raise - print(f"Registered endpoint: {key} = {value}") + print(f"Registered endpoint: {key} = {value}") + return + raise RuntimeError( + f"Failed to set key '{key}' in ConfigMap '{self.configmap_name}' " + f"after {_max_retries} attempts due to conflicts" + ) def get(self, key: str, timeout: float = 600, poll_interval: float = 2) -> str: """Poll until a key appears in the ConfigMap, then return its value. diff --git a/nemo_rl/distributed/standalone_gym_server.py b/nemo_rl/distributed/standalone_gym_server.py index 650ba48ac41..32c611e6774 100644 --- a/nemo_rl/distributed/standalone_gym_server.py +++ b/nemo_rl/distributed/standalone_gym_server.py @@ -122,15 +122,20 @@ def main(): loaded = OmegaConf.load(args.config_yaml) initial_global_config_dict = OmegaConf.to_container(loaded, resolve=False) - def _strip_interpolations(d): + def _strip_interpolations(d, path=""): if isinstance(d, dict): - return { - k: _strip_interpolations(v) - for k, v in d.items() - if not (isinstance(v, str) and "${" in v) - } + result = {} + for k, v in d.items(): + key_path = f"{path}.{k}" if path else k + if isinstance(v, str) and "${" in v: + print( + f"Dropping unresolvable interpolation: {key_path} = {v!r}" + ) + else: + result[k] = _strip_interpolations(v, key_path) + return result if isinstance(d, list): - return [_strip_interpolations(item) for item in d] + return [_strip_interpolations(item, f"{path}[]") for item in d] return d initial_global_config_dict = _strip_interpolations(initial_global_config_dict) diff --git a/nemo_rl/environments/nemo_gym.py b/nemo_rl/environments/nemo_gym.py index aa4200f4c39..d3eb6d5444d 100644 --- a/nemo_rl/environments/nemo_gym.py +++ b/nemo_rl/environments/nemo_gym.py @@ -13,6 +13,7 @@ # limitations under the License. from pathlib import Path from typing import Any, Dict, List, NotRequired, TypedDict +from urllib.parse import urlparse import ray import torch @@ -48,12 +49,14 @@ def __init__(self, cfg: NemoGymConfig): # No local subprocesses are spawned. remote_url = cfg["remote_gym_url"] # Parse host:port from URL like "http://gym-service:8080" or "gym-service:8080" - url = remote_url.removeprefix("http://").removeprefix("https://") - if ":" in url: - host, port_str = url.rsplit(":", 1) - port = int(port_str.rstrip("/")) - else: - host, port = url.rstrip("/"), 8080 + # Prefix with http:// if no scheme so urlparse handles it correctly. + parsed = urlparse( + remote_url + if remote_url.startswith(("http://", "https://")) + else f"http://{remote_url}" + ) + host = parsed.hostname or "localhost" + port = parsed.port or 8080 print(f"NemoGym remote mode: connecting to {host}:{port}") self.rh = None @@ -61,9 +64,12 @@ def __init__(self, cfg: NemoGymConfig): self.rch = RolloutCollectionHelper() initial_global_config_dict = cfg.get("initial_global_config_dict") or {} - self.rollout_max_attempts_to_avoid_lp_nan = initial_global_config_dict.pop( + self.rollout_max_attempts_to_avoid_lp_nan = initial_global_config_dict.get( "rollout_max_attempts_to_avoid_lp_nan", 1 ) + assert self.rollout_max_attempts_to_avoid_lp_nan >= 1, ( + "`rollout_max_attempts_to_avoid_lp_nan` must be at least 1" + ) else: # Colocated mode: spawn Gym subprocesses locally (original behavior). self._init_colocated(cfg) diff --git a/pyproject.toml b/pyproject.toml index a36afa804cd..9af8b5d1ee1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,6 +113,7 @@ nvrx = [ "nvidia-resiliency-ext", ] # for ft_launcher (fault-tolerant training launcher) nemo_gym = ["nemo_gym"] +k8s = ["kubernetes"] [dependency-groups] diff --git a/uv.lock b/uv.lock index 8baec5544e8..e9852371b15 100644 --- a/uv.lock +++ b/uv.lock @@ -1,46 +1,61 @@ version = 1 revision = 3 -requires-python = ">=3.13.13" +requires-python = ">=3.12" resolution-markers = [ - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", ] conflicts = [[ { package = "nemo-rl", extra = "fsdp" }, @@ -78,14 +93,8 @@ members = [ constraints = [ { name = "aiohttp", specifier = ">=3.13.3" }, { name = "brotli", specifier = ">=1.2.0" }, - { name = "cbor2", specifier = ">=5.9.0" }, - { name = "cryptography", specifier = ">=46.0.6" }, - { name = "onnx", specifier = ">=1.21.0rc4" }, - { name = "orjson", specifier = ">=3.11.6" }, { name = "protobuf", specifier = ">=6.33.5" }, - { name = "pyasn1", specifier = ">=0.6.3" }, - { name = "pygments", specifier = ">=2.20.0" }, - { name = "pyjwt", specifier = ">=2.12.0" }, + { name = "pyasn1", specifier = ">=0.6.2" }, { name = "python-multipart", specifier = ">=0.0.22" }, { name = "starlette", specifier = ">=0.49.1" }, { name = "urllib3", specifier = ">=2.6.3" }, @@ -93,26 +102,21 @@ constraints = [ ] overrides = [ { name = "deep-ep", git = "https://github.com/deepseek-ai/DeepEP.git?rev=bfded34800dfec415b71503f8205181de90b2480" }, - { name = "flashinfer-cubin", specifier = ">=0.5.0" }, - { name = "flashinfer-python", specifier = ">=0.5.0" }, + { name = "flashinfer-cubin", specifier = "==0.6.4" }, + { name = "flashinfer-python", specifier = "==0.6.4" }, { name = "llguidance", specifier = ">=1.3.0,<1.4.0" }, - { name = "mlflow", specifier = ">=3.11.1" }, { name = "nvidia-cudnn-cu12", specifier = "==9.19.0.56" }, - { name = "nvidia-cutlass-dsl", specifier = ">=4.4.1" }, + { name = "nvidia-cutlass-dsl", specifier = ">=4.2.1" }, { name = "nvidia-modelopt", extras = ["torch"], specifier = ">=0.39.0" }, { name = "opencv-python-headless", specifier = ">=4.11.0" }, { name = "opentelemetry-api", specifier = ">=1.33.1" }, - { name = "outlines", specifier = ">=0.2.0" }, - { name = "pytest", specifier = ">=9.0.3" }, { name = "setuptools", specifier = ">=80.10.2" }, { name = "timm", specifier = "<=1.0.22" }, { name = "torch", marker = "sys_platform != 'darwin'", specifier = "==2.10.0", index = "https://download.pytorch.org/whl/cu129" }, { name = "torch", marker = "sys_platform == 'darwin'", specifier = "==2.10.0", index = "https://pypi.org/simple" }, - { name = "torchaudio", marker = "sys_platform != 'darwin'", specifier = "==2.10.0", index = "https://download.pytorch.org/whl/cu129" }, - { name = "torchaudio", marker = "sys_platform == 'darwin'", specifier = "==2.10.0", index = "https://pypi.org/simple" }, - { name = "transformer-engine", extras = ["pytorch", "core-cu12"], git = "https://github.com/NVIDIA/TransformerEngine.git?rev=71bbefbf153418f943640df0f7373625dc93fa46" }, - { name = "transformers", specifier = "==5.3.0" }, - { name = "xgrammar", specifier = "==0.1.33" }, + { name = "torchaudio", specifier = "==2.10.0" }, + { name = "transformer-engine", extras = ["pytorch"], specifier = "==2.12.0" }, + { name = "transformers", specifier = "==5.2.0" }, ] [[manifest.dependency-metadata]] @@ -130,20 +134,10 @@ name = "deep-gemm" version = "2.0.0+7b6b556" requires-dist = ["torch", "packaging", "ninja"] -[[manifest.dependency-metadata]] -name = "drain3" -version = "0.9.11" -requires-dist = ["jsonpickle", "cachetools>=4.2.1"] - [[manifest.dependency-metadata]] name = "flash-attn" requires-dist = ["torch", "einops", "setuptools", "psutil", "ninja"] -[[manifest.dependency-metadata]] -name = "logsage" -version = "0.1.5" -requires-dist = ["drain3>=0.9.11,<0.10.0", "langchain>=0.3.27,<0.4.0", "langchain-core>=0.3.0,<1.0.0", "langchain-nvidia-ai-endpoints>=0.3.18,<0.4.0", "nh3>=0.3.1,<0.4.0", "numpy", "pandas", "pydantic-settings>=2.11.0,<3.0.0", "requests>=2.32.5,<3.0.0"] - [[manifest.dependency-metadata]] name = "mamba-ssm" version = "2.2.6.post3" @@ -152,7 +146,7 @@ requires-dist = ["torch", "packaging", "ninja", "causal-conv1d"] [[manifest.dependency-metadata]] name = "megatron-bridge" version = "0.0.0" -requires-dist = ["transformers>=5.0.0,<=5.3.0", "peft>=0.18.1", "datasets>=2.20.0", "accelerate", "diffusers>=0.36.0", "peft>=0.18.0", "einops", "imageio", "imageio-ffmpeg", "omegaconf>=2.3.0", "tensorboard>=2.19.0", "typing-extensions", "rich", "wandb>=0.25.0", "six>=1.17.0", "regex>=2024.11.6", "pyyaml>=6.0.2", "tqdm>=4.67.1", "hydra-core>1.3,<=1.3.2", "qwen-vl-utils", "transformer-engine[pytorch,core-cu12]", "mamba-ssm", "nvidia-resiliency-ext", "causal-conv1d", "flash-linear-attention", "timm", "open-clip-torch>=3.2.0", "mlflow>=3.9.0", "comet-ml>=3.50.0", "torch>=2.6.0"] +requires-dist = ["transformers>=5.0.0,<=5.2.0", "datasets>=2.20.0", "accelerate", "omegaconf>=2.3.0", "tensorboard>=2.19.0", "typing-extensions", "rich", "wandb>=0.25.0", "six>=1.17.0", "regex>=2024.11.6", "pyyaml>=6.0.2", "tqdm>=4.67.1", "hydra-core>1.3,<=1.3.2", "qwen-vl-utils", "transformer-engine[pytorch,core-cu12]", "mamba-ssm", "nvidia-resiliency-ext~=0.4.1", "causal-conv1d", "flash-linear-attention", "timm", "open-clip-torch>=3.2.0", "mlflow>=3.5.0", "torch>=2.6.0"] [[manifest.dependency-metadata]] name = "nv-grouped-gemm" @@ -160,24 +154,14 @@ version = "1.1.4.post7" requires-dist = ["setuptools", "wheel", "torch", "numpy"] [[manifest.dependency-metadata]] -name = "sglang" -version = "0.5.10" -requires-dist = ["ipython", "aiohttp", "apache-tvm-ffi>=0.1.5,<0.2", "anthropic>=0.20.0", "blobfile==3.0.0", "build", "compressed-tensors", "cuda-python==12.9", "decord2", "datasets", "einops", "fastapi", "flashinfer-python==0.6.7.post2", "flashinfer-cubin==0.6.7.post2", "gguf", "interegular", "llguidance>=0.7.11,<0.8.0", "modelscope", "msgspec", "ninja", "numpy", "nvidia-cutlass-dsl>=4.4.1", "nvidia-ml-py", "openai-harmony==0.0.4", "openai==2.6.1", "orjson", "outlines==0.1.11", "packaging", "partial-json-parser", "pillow", "prometheus-client>=0.20.0", "psutil", "py-spy", "pybase64", "pydantic", "python-multipart", "pyzmq>=25.1.2", "quack-kernels>=0.3.0", "requests", "scipy", "sentencepiece", "setproctitle", "flash-attn-4>=4.0.0b4", "sglang-kernel==0.4.1", "soundfile==0.13.1", "tiktoken", "timm==1.0.16", "torch-memory-saver==0.0.9", "torch==2.9.1", "torchao==0.9.0", "torchaudio==2.9.1", "torchcodec==0.9.1 ; (platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l') or sys_platform != 'linux'", "torchvision", "tqdm", "mistral-common>=1.9.0", "transformers==5.3.0", "uvicorn", "uvloop", "watchfiles", "xgrammar==0.1.32", "smg-grpc-servicer>=0.5.0"] - -[[manifest.dependency-metadata]] -name = "sglang-kernel" -version = "0.4.1" +name = "sgl-kernel" +version = "0.3.21" requires-dist = ["torch", "scikit-build-core", "wheel"] [[manifest.dependency-metadata]] -name = "transformer-engine" -version = "2.14.0+71bbefb" -requires-dist = ["torch", "pydantic", "importlib-metadata>=1.0", "packaging"] - -[[manifest.dependency-metadata]] -name = "transformer-engine-torch" -version = "2.14.0+71bbefb" -requires-dist = ["torch", "transformer-engine"] +name = "sglang" +version = "0.5.7.dev0" +requires-dist = ["ipython", "aiohttp", "apache-tvm-ffi>=0.1.5,<0.2", "anthropic>=0.20.0", "blobfile==3.0.0", "build", "compressed-tensors", "cuda-python==12.9", "decord2", "datasets", "einops", "fastapi", "flashinfer-python==0.6.4", "flashinfer-cubin==0.6.4", "gguf", "interegular", "llguidance>=0.7.11,<0.8.0", "modelscope", "msgspec", "ninja", "numpy", "nvidia-cutlass-dsl>=4.3.4", "nvidia-ml-py", "openai-harmony==0.0.4", "openai==2.6.1", "orjson", "outlines==0.1.11", "packaging", "partial-json-parser", "pillow", "prometheus-client>=0.20.0", "psutil", "py-spy", "pybase64", "pydantic", "python-multipart", "pyzmq>=25.1.2", "quack-kernels==0.2.4", "requests", "scipy", "sentencepiece", "setproctitle", "sgl-kernel==0.3.21", "soundfile==0.13.1", "tiktoken", "timm==1.0.16", "torch-memory-saver==0.0.9", "torch==2.9.1", "torchao==0.9.0", "torchaudio==2.9.1", "torchcodec==0.8.0 ; (platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l') or sys_platform != 'linux'", "torchvision", "tqdm", "transformers==5.2.0", "uvicorn", "uvloop", "xgrammar==0.1.27", "smg-grpc-proto>=0.3.3", "grpcio>=1.78.0", "grpcio-reflection>=1.78.0", "grpcio-health-checking>=1.78.0"] [[package]] name = "absl-py" @@ -194,8 +178,8 @@ version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "packaging" }, { name = "psutil" }, { name = "pyyaml" }, @@ -271,6 +255,23 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, @@ -351,12 +352,22 @@ version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] 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 = "airportsdata" +version = "20250909" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/c6/17ae8a65f7fa5bbbeee166f8070063eb8b70c89501a65c2e6885db61fc08/airportsdata-20250909.tar.gz", hash = "sha256:f39974fe1101817ced4ccf7c6ed336408469e5e778395d0a3e7a5112ec298f90", size = 907204, upload-time = "2025-09-09T01:07:31.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/87/59b570b9c4b014532777dc3baffc9bea10cf0cc8b232cf3c17e4bd0754a6/airportsdata-20250909-py3-none-any.whl", hash = "sha256:ce7dc6e1485afe3915e708212c7024ad158470c1c934e6a6cb217cf28b798ac7", size = 914391, upload-time = "2025-09-09T01:07:29.364Z" }, +] + [[package]] name = "alabaster" version = "1.0.0" @@ -430,6 +441,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } wheels = [ @@ -498,12 +510,77 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] +[[package]] +name = "audioop-lts" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686, upload-time = "2025-08-05T16:43:17.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523, upload-time = "2025-08-05T16:42:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455, upload-time = "2025-08-05T16:42:22.283Z" }, + { url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997, upload-time = "2025-08-05T16:42:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844, upload-time = "2025-08-05T16:42:25.208Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056, upload-time = "2025-08-05T16:42:26.559Z" }, + { url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892, upload-time = "2025-08-05T16:42:27.902Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660, upload-time = "2025-08-05T16:42:28.9Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143, upload-time = "2025-08-05T16:42:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313, upload-time = "2025-08-05T16:42:30.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044, upload-time = "2025-08-05T16:42:31.959Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766, upload-time = "2025-08-05T16:42:33.302Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640, upload-time = "2025-08-05T16:42:34.854Z" }, + { url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052, upload-time = "2025-08-05T16:42:35.839Z" }, + { url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185, upload-time = "2025-08-05T16:42:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503, upload-time = "2025-08-05T16:42:38.427Z" }, + { url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173, upload-time = "2025-08-05T16:42:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096, upload-time = "2025-08-05T16:42:40.684Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748, upload-time = "2025-08-05T16:42:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329, upload-time = "2025-08-05T16:42:42.987Z" }, + { url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407, upload-time = "2025-08-05T16:42:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811, upload-time = "2025-08-05T16:42:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470, upload-time = "2025-08-05T16:42:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878, upload-time = "2025-08-05T16:42:47.576Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867, upload-time = "2025-08-05T16:42:49.003Z" }, + { url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001, upload-time = "2025-08-05T16:42:50.038Z" }, + { url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046, upload-time = "2025-08-05T16:42:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788, upload-time = "2025-08-05T16:42:52.198Z" }, + { url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472, upload-time = "2025-08-05T16:42:53.59Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279, upload-time = "2025-08-05T16:42:54.632Z" }, + { url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568, upload-time = "2025-08-05T16:42:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942, upload-time = "2025-08-05T16:42:56.674Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603, upload-time = "2025-08-05T16:42:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104, upload-time = "2025-08-05T16:42:58.518Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754, upload-time = "2025-08-05T16:43:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332, upload-time = "2025-08-05T16:43:01.666Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396, upload-time = "2025-08-05T16:43:02.991Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811, upload-time = "2025-08-05T16:43:04.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483, upload-time = "2025-08-05T16:43:05.085Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885, upload-time = "2025-08-05T16:43:06.197Z" }, + { url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899, upload-time = "2025-08-05T16:43:07.291Z" }, + { url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998, upload-time = "2025-08-05T16:43:08.335Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046, upload-time = "2025-08-05T16:43:09.367Z" }, + { url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843, upload-time = "2025-08-05T16:43:10.749Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490, upload-time = "2025-08-05T16:43:12.131Z" }, + { url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297, upload-time = "2025-08-05T16:43:13.139Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331, upload-time = "2025-08-05T16:43:14.19Z" }, + { url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697, upload-time = "2025-08-05T16:43:15.193Z" }, + { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" }, +] + [[package]] name = "av" version = "15.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/17/89/940a509ee7e9449f0c877fa984b37b7cc485546035cc67bbc353f2ac20f3/av-15.0.0.tar.gz", hash = "sha256:871c1a9becddf00b60b1294dc0bff9ff193ac31286aeec1a34039bd27e650183", size = 3833128, upload-time = "2025-07-03T16:23:48.455Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/89/81/c5d009ea9c01a513b7af6aac2ac49c0f2f7193345071cd6dd4d91bef3ab9/av-15.0.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:84e2ede9459e64e768f4bc56d9df65da9e94b704ee3eccfe2e5b1da1da754313", size = 21782026, upload-time = "2025-07-03T16:22:18.41Z" }, + { url = "https://files.pythonhosted.org/packages/16/8a/ffe9fcac35a07efc6aa0d765015efa499d88823c01499f318760460f8088/av-15.0.0-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:9473ed92d6942c5a449a2c79d49f3425eb0272499d1a3559b32c1181ff736a08", size = 26974939, upload-time = "2025-07-03T16:22:21.493Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e7/0816e52134dc2d0259bb1aaad78573eacaf2bebc1a643de34e3384b520d6/av-15.0.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:56a53fe4e09bebd99355eaa0ce221b681eaf205bdda114f5e17fb79f3c3746ad", size = 34573486, upload-time = "2025-07-03T16:22:24.684Z" }, + { url = "https://files.pythonhosted.org/packages/a3/f4/07cc05712e9824a4bb68beea44eb5a7369dee3f00fa258879190004b7fc5/av-15.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:247dd9a99d7ed3577b8c1e9977e811f423b04504ff36c9dcd7a4de3e6e5fe5ad", size = 38418908, upload-time = "2025-07-03T16:22:27.799Z" }, + { url = "https://files.pythonhosted.org/packages/19/48/7f3a21a41e291f8c5b8a98f95cfef308ce1b024a634413ce910c270efd7d/av-15.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:fc50a7d5f60109221ccf44f8fa4c56ce73f22948b7f19b1717fcc58f7fbc383e", size = 40010257, upload-time = "2025-07-03T16:22:31.15Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c9/ced392e82d39084544d2d0c05decb36446028928eddf0d40ec3d8fe6c050/av-15.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:77deaec8943abfebd4e262924f2f452d6594cf0bc67d8d98aac0462b476e4182", size = 40381801, upload-time = "2025-07-03T16:22:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a23ad111200e27f5773e94b0b6f9e2ea492a72ded7f4787a358d9d504a8b/av-15.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:601d9b0740e47a17ec96ba2a537ebfd4d6edc859ae6f298475c06caa51f0a019", size = 37219417, upload-time = "2025-07-03T16:22:37.497Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/2ac20143b74e3792ede40bfd397ce72fa4e76a03999c2fd0aee3997b6971/av-15.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e021f67e0db7256c9f5d3d6a2a4237a4a4a804b131b33e7f2778981070519b20", size = 41242077, upload-time = "2025-07-03T16:22:40.86Z" }, + { url = "https://files.pythonhosted.org/packages/bd/30/40452705dffbfef0f5505d36218970dfeff0a86048689910219c8717b310/av-15.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:383f1b57520d790069d85fc75f43cfa32fca07f5fb3fb842be37bd596638602c", size = 31357617, upload-time = "2025-07-03T16:22:43.934Z" }, { url = "https://files.pythonhosted.org/packages/a6/27/c2e248498ce78dd504b0b1818ce88e71e30a7e26c348bdf5d6467d7b06f7/av-15.0.0-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:0701c116f32bd9478023f610722f6371d15ca0c068ff228d355f54a7cf23d9cb", size = 21746400, upload-time = "2025-07-03T16:22:46.604Z" }, { url = "https://files.pythonhosted.org/packages/1d/d8/11f8452f19f4ddc189e978b215420131db40e3919135c14a0d13520f7c94/av-15.0.0-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:57fb6232494ec575b8e78e5a9ef9b811d78f8d67324476ec8430ca3146751124", size = 26939576, upload-time = "2025-07-03T16:22:49.255Z" }, { url = "https://files.pythonhosted.org/packages/00/1c/b109fd41487d91b8843f9e199b65e89ca533a612ec788b11ed0ba9812ea3/av-15.0.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:801a3e0afd5c36df70d012d083bfca67ab22d0ebd2c860c0d9432ac875bc0ad6", size = 34284344, upload-time = "2025-07-03T16:22:52.373Z" }, @@ -543,6 +620,19 @@ version = "3.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/95/06/92fdc84448d324ab8434b78e65caf4fb4c6c90b4f8ad9bdd4c8021bfaf1e/bitarray-3.8.0.tar.gz", hash = "sha256:3eae38daffd77c9621ae80c16932eea3fb3a4af141fb7cc724d4ad93eff9210d", size = 151991, upload-time = "2025-11-02T21:41:15.117Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/82/a0/0c41d893eda756315491adfdbf9bc928aee3d377a7f97a8834d453aa5de1/bitarray-3.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2fcbe9b3a5996b417e030aa33a562e7e20dfc86271e53d7e841fc5df16268b8", size = 148575, upload-time = "2025-11-02T21:39:25.718Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/12ab2f4a4429bd844b419c37877caba93d676d18be71354fbbeb21d9f4cc/bitarray-3.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd761d158f67e288fd0ebe00c3b158095ce80a4bc7c32b60c7121224003ba70d", size = 145454, upload-time = "2025-11-02T21:39:26.695Z" }, + { url = "https://files.pythonhosted.org/packages/26/58/314b3e3f219533464e120f0c51ac5123e7b1c1b91f725a4073fb70c5a858/bitarray-3.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c394a3f055b49f92626f83c1a0b6d6cd2c628f1ccd72481c3e3c6aa4695f3b20", size = 332949, upload-time = "2025-11-02T21:39:27.801Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ce/ca8c706bd8341c7a22dd92d2a528af71f7e5f4726085d93f81fd768cb03b/bitarray-3.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:969fd67de8c42affdb47b38b80f1eaa79ac0ef17d65407cdd931db1675315af1", size = 360599, upload-time = "2025-11-02T21:39:28.964Z" }, + { url = "https://files.pythonhosted.org/packages/ef/dc/aa181df85f933052d962804906b282acb433cb9318b08ec2aceb4ee34faf/bitarray-3.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:99d25aff3745c54e61ab340b98400c52ebec04290a62078155e0d7eb30380220", size = 371972, upload-time = "2025-11-02T21:39:30.228Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d9/b805bfa158c7bcf4df0ac19b1be581b47e1ddb792c11023aed80a7058e78/bitarray-3.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e645b4c365d6f1f9e0799380ad6395268f3c3b898244a650aaeb8d9d27b74c35", size = 340303, upload-time = "2025-11-02T21:39:31.342Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/5308cc97ea929e30727292617a3a88293470166851e13c9e3f16f395da55/bitarray-3.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2fa23fdb3beab313950bbb49674e8a161e61449332d3997089fe3944953f1b77", size = 330494, upload-time = "2025-11-02T21:39:32.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/89/64f1596cb80433323efdbc8dcd0d6e57c40dfbe6ea3341623f34ec397edd/bitarray-3.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:165052a0e61c880f7093808a0c524ce1b3555bfa114c0dfb5c809cd07918a60d", size = 358123, upload-time = "2025-11-02T21:39:34.331Z" }, + { url = "https://files.pythonhosted.org/packages/27/fd/f3d49c5443b57087f888b5e118c8dd78bb7c8e8cfeeed250f8e92128a05f/bitarray-3.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:337c8cd46a4c6568d367ed676cbf2d7de16f890bb31dbb54c44c1d6bb6d4a1de", size = 356046, upload-time = "2025-11-02T21:39:35.449Z" }, + { url = "https://files.pythonhosted.org/packages/aa/db/1fd0b402bd2b47142e958b6930dbb9445235d03fa703c9a24caa6e576ae2/bitarray-3.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21ca6a47bf20db9e7ad74ca04b3d479e4d76109b68333eb23535553d2705339e", size = 336872, upload-time = "2025-11-02T21:39:36.891Z" }, + { url = "https://files.pythonhosted.org/packages/58/73/680b47718f1313b4538af479c4732eaca0aeda34d93fc5b869f87932d57d/bitarray-3.8.0-cp312-cp312-win32.whl", hash = "sha256:178c5a4c7fdfb5cd79e372ae7f675390e670f3732e5bc68d327e01a5b3ff8d55", size = 143025, upload-time = "2025-11-02T21:39:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/f8/11/7792587c19c79a8283e8838f44709fa4338a8f7d2a3091dfd81c07ae89c7/bitarray-3.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:75a3b6e9c695a6570ea488db75b84bb592ff70a944957efa1c655867c575018b", size = 149969, upload-time = "2025-11-02T21:39:39.715Z" }, + { url = "https://files.pythonhosted.org/packages/9a/00/9df64b5d8a84e8e9ec392f6f9ce93f50626a5b301cb6c6b3fe3406454d66/bitarray-3.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:5591daf81313096909d973fb2612fccd87528fdfdd39f6478bdce54543178954", size = 146907, upload-time = "2025-11-02T21:39:40.815Z" }, { url = "https://files.pythonhosted.org/packages/3e/35/480364d4baf1e34c79076750914664373f561c58abb5c31c35b3fae613ff/bitarray-3.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:18214bac86341f1cc413772e66447d6cca10981e2880b70ecaf4e826c04f95e9", size = 148582, upload-time = "2025-11-02T21:39:42.268Z" }, { url = "https://files.pythonhosted.org/packages/5e/a8/718b95524c803937f4edbaaf6480f39c80f6ed189d61357b345e8361ffb6/bitarray-3.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:01c5f0dc080b0ebb432f7a68ee1e88a76bd34f6d89c9568fcec65fb16ed71f0e", size = 145433, upload-time = "2025-11-02T21:39:43.552Z" }, { url = "https://files.pythonhosted.org/packages/03/66/4a10f30dc9e2e01e3b4ecd44a511219f98e63c86b0e0f704c90fac24059b/bitarray-3.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86685fa04067f7175f9718489ae755f6acde03593a1a9ca89305554af40e14fd", size = 332986, upload-time = "2025-11-02T21:39:44.656Z" }, @@ -602,6 +692,18 @@ version = "1.0.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/75/aa/abcd75e9600987a0bc6cfe9b6b2ff3f0e2cb08c170addc6e76035b5c4cb3/blake3-1.0.8.tar.gz", hash = "sha256:513cc7f0f5a7c035812604c2c852a0c1468311345573de647e310aca4ab165ba", size = 117308, upload-time = "2025-10-14T06:47:48.83Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/a0/b7b6dff04012cfd6e665c09ee446f749bd8ea161b00f730fe1bdecd0f033/blake3-1.0.8-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8da4233984d51471bd4e4366feda1d90d781e712e0a504ea54b1f2b3577557b", size = 347983, upload-time = "2025-10-14T06:45:47.214Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a2/264091cac31d7ae913f1f296abc20b8da578b958ffb86100a7ce80e8bf5c/blake3-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1257be19f2d381c868a34cc822fc7f12f817ddc49681b6d1a2790bfbda1a9865", size = 325415, upload-time = "2025-10-14T06:45:48.482Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/85a4c0782f613de23d114a7a78fcce270f75b193b3ff3493a0de24ba104a/blake3-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:269f255b110840e52b6ce9db02217e39660ebad3e34ddd5bca8b8d378a77e4e1", size = 371296, upload-time = "2025-10-14T06:45:49.674Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/488475254976ed93fab57c67aa80d3b40df77f7d9db6528c9274bff53e08/blake3-1.0.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66ca28a673025c40db3eba21a9cac52f559f83637efa675b3f6bd8683f0415f3", size = 374516, upload-time = "2025-10-14T06:45:51.23Z" }, + { url = "https://files.pythonhosted.org/packages/7b/21/2a1c47fedb77fb396512677ec6d46caf42ac6e9a897db77edd0a2a46f7bb/blake3-1.0.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcb04966537777af56c1f399b35525aa70a1225816e121ff95071c33c0f7abca", size = 447911, upload-time = "2025-10-14T06:45:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7d/db0626df16029713e7e61b67314c4835e85c296d82bd907c21c6ea271da2/blake3-1.0.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5b5da177d62cc4b7edf0cea08fe4dec960c9ac27f916131efa890a01f747b93", size = 505420, upload-time = "2025-10-14T06:45:54.445Z" }, + { url = "https://files.pythonhosted.org/packages/5b/55/6e737850c2d58a6d9de8a76dad2ae0f75b852a23eb4ecb07a0b165e6e436/blake3-1.0.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38209b10482c97e151681ea3e91cc7141f56adbbf4820a7d701a923124b41e6a", size = 394189, upload-time = "2025-10-14T06:45:55.719Z" }, + { url = "https://files.pythonhosted.org/packages/5b/94/eafaa5cdddadc0c9c603a6a6d8339433475e1a9f60c8bb9c2eed2d8736b6/blake3-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504d1399b7fb91dfe5c25722d2807990493185faa1917456455480c36867adb5", size = 388001, upload-time = "2025-10-14T06:45:57.067Z" }, + { url = "https://files.pythonhosted.org/packages/17/81/735fa00d13de7f68b25e1b9cb36ff08c6f165e688d85d8ec2cbfcdedccc5/blake3-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c84af132aa09abeadf9a0118c8fb26f4528f3f42c10ef8be0fcf31c478774ec4", size = 550302, upload-time = "2025-10-14T06:45:58.657Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c6/d1fe8bdea4a6088bd54b5a58bc40aed89a4e784cd796af7722a06f74bae7/blake3-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a25db3d36b55f5ed6a86470155cc749fc9c5b91c949b8d14f48658f9d960d9ec", size = 554211, upload-time = "2025-10-14T06:46:00.269Z" }, + { url = "https://files.pythonhosted.org/packages/55/d1/ca74aa450cbe10e396e061f26f7a043891ffa1485537d6b30d3757e20995/blake3-1.0.8-cp312-cp312-win32.whl", hash = "sha256:e0fee93d5adcd44378b008c147e84f181f23715307a64f7b3db432394bbfce8b", size = 228343, upload-time = "2025-10-14T06:46:01.533Z" }, + { url = "https://files.pythonhosted.org/packages/4d/42/bbd02647169e3fbed27558555653ac2578c6f17ccacf7d1956c58ef1d214/blake3-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:6a6eafc29e4f478d365a87d2f25782a521870c8514bb43734ac85ae9be71caf7", size = 215704, upload-time = "2025-10-14T06:46:02.79Z" }, { url = "https://files.pythonhosted.org/packages/55/b8/11de9528c257f7f1633f957ccaff253b706838d22c5d2908e4735798ec01/blake3-1.0.8-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:46dc20976bd6c235959ef0246ec73420d1063c3da2839a9c87ca395cf1fd7943", size = 347771, upload-time = "2025-10-14T06:46:04.248Z" }, { url = "https://files.pythonhosted.org/packages/50/26/f7668be55c909678b001ecacff11ad7016cd9b4e9c7cc87b5971d638c5a9/blake3-1.0.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d17eb6382634b3a5bc0c0e0454d5265b0becaeeadb6801ed25150b39a999d0cc", size = 325431, upload-time = "2025-10-14T06:46:06.136Z" }, { url = "https://files.pythonhosted.org/packages/77/57/e8a85fa261894bf7ce7af928ff3408aab60287ab8d58b55d13a3f700b619/blake3-1.0.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19fc6f2b7edab8acff6895fc6e38c19bd79f4c089e21153020c75dfc7397d52d", size = 370994, upload-time = "2025-10-14T06:46:07.398Z" }, @@ -722,6 +824,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, ] +[[package]] +name = "brotli" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" }, + { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" }, + { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362, upload-time = "2025-11-05T18:38:32.939Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115, upload-time = "2025-11-05T18:38:33.765Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" }, + { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" }, + { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" }, + { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" }, + { url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" }, + { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" }, + { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" }, +] + [[package]] name = "build" version = "1.4.0" @@ -758,25 +898,32 @@ dependencies = [ [[package]] name = "cbor2" -version = "5.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/cb/09939728be094d155b5d4ac262e39877875f5f7e36eea66beb359f647bd0/cbor2-5.9.0.tar.gz", hash = "sha256:85c7a46279ac8f226e1059275221e6b3d0e370d2bb6bd0500f9780781615bcea", size = 111231, upload-time = "2026-03-22T15:56:50.638Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c5/4901e21a8afe9448fd947b11e8f383903207cd6dd0800e5f5a386838de5b/cbor2-5.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fbb06f34aa645b4deca66643bba3d400d20c15312d1fe88d429be60c1ab50f27", size = 71284, upload-time = "2026-03-22T15:56:22.836Z" }, - { url = "https://files.pythonhosted.org/packages/1b/10/df643a381aebc3f05486de4813662bc58accb640fc3275cb276a75e89694/cbor2-5.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac684fe195c39821fca70d18afbf748f728aefbfbf88456018d299e559b8cae0", size = 287682, upload-time = "2026-03-22T15:56:24.024Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/8aa6b766059ae4a0ca1ec3ff96fe3823a69a7be880dba2e249f7fbe2700b/cbor2-5.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a54fbb32cb828c214f7f333a707e4aec61182e7efdc06ea5d9596d3ecee624a", size = 288009, upload-time = "2026-03-22T15:56:25.305Z" }, - { url = "https://files.pythonhosted.org/packages/74/07/6236bc25c183a9cf7e8062e5dddf9eae9b0b14ebf14a58a69fe5a1e872c6/cbor2-5.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4753a6d1bc71054d9179557bc65740860f185095ccb401d46637fff028a5b3ec", size = 280437, upload-time = "2026-03-22T15:56:26.479Z" }, - { url = "https://files.pythonhosted.org/packages/4e/0a/84328d23c3c68874ac6497edb9b1900579a1028efa54734df3f1762bbc15/cbor2-5.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:380e534482b843e43442b87d8777a7bf9bed20cb7526f89b780c3400f617304b", size = 282247, upload-time = "2026-03-22T15:56:28.644Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f6/89b4627e09d028c8e5fcaf7cb55f225c33ce6e037ec1844e65d02bcfa945/cbor2-5.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:dcf0f695873e5c94bd072d6af8698e72b8fb7f7a18f37e0bced1041b7111a6cf", size = 70089, upload-time = "2026-03-22T15:56:29.801Z" }, - { url = "https://files.pythonhosted.org/packages/e2/7c/efadcd5f0102db692490e4e206988a2f98d39a09912090db497a2b800885/cbor2-5.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:f7c9751a9611601ab326d8f5837f01379195bbf06175fb4effeb552140e7c9e8", size = 65466, upload-time = "2026-03-22T15:56:30.823Z" }, - { url = "https://files.pythonhosted.org/packages/08/7d/9ccc36d10ef96e6038e48046ebe1ce35a1e7814da0e1e204d09e6ef09b8d/cbor2-5.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23606d31ba1368bd1b6602e3020ee88fe9523ca80e8630faf6b2fc904fd84560", size = 71500, upload-time = "2026-03-22T15:56:31.876Z" }, - { url = "https://files.pythonhosted.org/packages/70/e1/a6cca2cc72e13f00030c6a649f57ae703eb2c620806ab70c40db8eab33fa/cbor2-5.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0322296b9d52f55880e300ba8ba09ecf644303b99b51138bbb1c0fb644fa7c3e", size = 286953, upload-time = "2026-03-22T15:56:33.292Z" }, - { url = "https://files.pythonhosted.org/packages/08/3c/24cd5ef488a957d90e016f200a3aad820e4c2f85edd61c9fe4523007a1ee/cbor2-5.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:422817286c1d0ce947fb2f7eca9212b39bddd7231e8b452e2d2cc52f15332dba", size = 285454, upload-time = "2026-03-22T15:56:34.703Z" }, - { url = "https://files.pythonhosted.org/packages/a4/35/dca96818494c0ba47cdd73e8d809b27fa91f8fa0ce32a068a09237687454/cbor2-5.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9a4907e0c3035bb8836116854ed8e56d8aef23909d601fa59706320897ec2551", size = 279441, upload-time = "2026-03-22T15:56:35.888Z" }, - { url = "https://files.pythonhosted.org/packages/a4/44/d3362378b16e53cf7e535a3f5aed8476e2109068154e24e31981ef5bde9e/cbor2-5.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fb7afe77f8d269e42d7c4b515c6fd14f1ccc0625379fb6829b269f493d16eddd", size = 279673, upload-time = "2026-03-22T15:56:37.08Z" }, - { url = "https://files.pythonhosted.org/packages/43/d1/3533a697e5842fff7c2f64912eb251f8dcab3a8b5d88e228d6eebc3b5021/cbor2-5.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:86baf870d4c0bfc6f79de3801f3860a84ab76d9c8b0abb7f081f2c14c38d79d3", size = 71940, upload-time = "2026-03-22T15:56:38.366Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e2/c6ba75f3fb25dfa15ab6999cc8709c821987e9ed8e375d7f58539261bcb9/cbor2-5.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:7221483fad0c63afa4244624d552abf89d7dfdbc5f5edfc56fc1ff2b4b818975", size = 67639, upload-time = "2026-03-22T15:56:39.39Z" }, - { url = "https://files.pythonhosted.org/packages/42/ff/b83492b096fbef26e9cb62c1a4bf2d3cef579ea7b33138c6c37c4ae66f67/cbor2-5.9.0-py3-none-any.whl", hash = "sha256:27695cbd70c90b8de5c4a284642c2836449b14e2c2e07e3ffe0744cb7669a01b", size = 24627, upload-time = "2026-03-22T15:56:48.847Z" }, +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/8e/8b4fdde28e42ffcd741a37f4ffa9fb59cd4fe01625b544dfcfd9ccb54f01/cbor2-5.8.0.tar.gz", hash = "sha256:b19c35fcae9688ac01ef75bad5db27300c2537eb4ee00ed07e05d8456a0d4931", size = 107825, upload-time = "2025-12-30T18:44:22.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/4f/3a16e3e8fd7e5fd86751a4f1aad218a8d19a96e75ec3989c3e95a8fe1d8f/cbor2-5.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b3f91fa699a5ce22470e973601c62dd9d55dc3ca20ee446516ac075fcab27c9", size = 70270, upload-time = "2025-12-30T18:43:46.005Z" }, + { url = "https://files.pythonhosted.org/packages/38/81/0d0cf0796fe8081492a61c45278f03def21a929535a492dd97c8438f5dbe/cbor2-5.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:518c118a5e00001854adb51f3164e647aa99b6a9877d2a733a28cb5c0a4d6857", size = 286242, upload-time = "2025-12-30T18:43:47.026Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/fdab6c10190cfb8d639e01f2b168f2406fc847a2a6bc00e7de78c3381d0a/cbor2-5.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cff2a1999e49cd51c23d1b6786a012127fd8f722c5946e82bd7ab3eb307443f3", size = 285412, upload-time = "2025-12-30T18:43:48.563Z" }, + { url = "https://files.pythonhosted.org/packages/31/59/746a8e630996217a3afd523f583fcf7e3d16640d63f9a03f0f4e4f74b5b1/cbor2-5.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c4492160212374973cdc14e46f0565f2462721ef922b40f7ea11e7d613dfb2a", size = 278041, upload-time = "2025-12-30T18:43:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/f3bbeb6dedd45c6e0cddd627ea790dea295eaf82c83f0e2159b733365ebd/cbor2-5.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:546c7c7c4c6bcdc54a59242e0e82cea8f332b17b4465ae628718fef1fce401ca", size = 278185, upload-time = "2025-12-30T18:43:51.192Z" }, + { url = "https://files.pythonhosted.org/packages/67/e5/9013d6b857ceb6cdb2851ffb5a887f53f2bab934a528c9d6fa73d9989d84/cbor2-5.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:074f0fa7535dd7fdee247c2c99f679d94f3aa058ccb1ccf4126cc72d6d89cbae", size = 69817, upload-time = "2025-12-30T18:43:52.352Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ab/7aa94ba3d44ecbc3a97bdb2fb6a8298063fe2e0b611e539a6fe41e36da20/cbor2-5.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:f95fed480b2a0d843f294d2a1ef4cc0f6a83c7922927f9f558e1f5a8dc54b7ca", size = 64923, upload-time = "2025-12-30T18:43:53.719Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0d/5a3f20bafaefeb2c1903d961416f051c0950f0d09e7297a3aa6941596b29/cbor2-5.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6d8d104480845e2f28c6165b4c961bbe58d08cb5638f368375cfcae051c28015", size = 70332, upload-time = "2025-12-30T18:43:54.694Z" }, + { url = "https://files.pythonhosted.org/packages/57/66/177a3f089e69db69c987453ab4934086408c3338551e4984734597be9f80/cbor2-5.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43efee947e5ab67d406d6e0dc61b5dee9d2f5e89ae176f90677a3741a20ca2e7", size = 285985, upload-time = "2025-12-30T18:43:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/9e17b8e4ed80a2ce97e2dfa5915c169dbb31599409ddb830f514b57f96cc/cbor2-5.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be7ae582f50be539e09c134966d0fd63723fc4789b8dff1f6c2e3f24ae3eaf32", size = 285173, upload-time = "2025-12-30T18:43:57.321Z" }, + { url = "https://files.pythonhosted.org/packages/cc/33/9f92e107d78f88ac22723ac15d0259d220ba98c1d855e51796317f4c4114/cbor2-5.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50f5c709561a71ea7970b4cd2bf9eda4eccacc0aac212577080fdfe64183e7f5", size = 278395, upload-time = "2025-12-30T18:43:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3f/46b80050a4a35ce5cf7903693864a9fdea7213567dc8faa6e25cb375c182/cbor2-5.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a6790ecc73aa93e76d2d9076fc42bf91a9e69f2295e5fa702e776dbe986465bd", size = 278330, upload-time = "2025-12-30T18:43:59.656Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/d41f8c04c783a4d204e364be2d38043d4f732a3bed6f4c732e321cf34c7b/cbor2-5.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:c114af8099fa65a19a514db87ce7a06e942d8fea2730afd49be39f8e16e7f5e0", size = 69841, upload-time = "2025-12-30T18:44:01.159Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8c/0397a82f6e67665009951453c83058e4c77ba54b9a9017ede56d6870306c/cbor2-5.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:ab3ba00494ad8669a459b12a558448d309c271fa4f89b116ad496ee35db38fea", size = 64982, upload-time = "2025-12-30T18:44:02.138Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0c/0654233d7543ac8a50f4785f172430ddc97538ba418eb305d6e529d1a120/cbor2-5.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ad72381477133046ce217617d839ea4e9454f8b77d9a6351b229e214102daeb7", size = 70710, upload-time = "2025-12-30T18:44:03.209Z" }, + { url = "https://files.pythonhosted.org/packages/84/62/4671d24e557d7f5a74a01b422c538925140c0495e57decde7e566f91d029/cbor2-5.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6da25190fad3434ce99876b11d4ca6b8828df6ca232cf7344cd14ae1166fb718", size = 285005, upload-time = "2025-12-30T18:44:05.109Z" }, + { url = "https://files.pythonhosted.org/packages/87/85/0c67d763a08e848c9a80d7e4723ba497cce676f41bc7ca1828ae90a0a872/cbor2-5.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c13919e3a24c5a6d286551fa288848a4cedc3e507c58a722ccd134e461217d99", size = 282435, upload-time = "2025-12-30T18:44:06.465Z" }, + { url = "https://files.pythonhosted.org/packages/b2/01/0650972b4dbfbebcfbe37cbba7fc3cd9019a8da6397ab3446e07175e342b/cbor2-5.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f8c40d32e5972047a777f9bf730870828f3cf1c43b3eb96fd0429c57a1d3b9e6", size = 277493, upload-time = "2025-12-30T18:44:07.609Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6c/7704a4f32adc7f10f3b41ec067f500a4458f7606397af5e4cf2d368fd288/cbor2-5.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7627894bc0b3d5d0807f31e3107e11b996205470c4429dc2bb4ef8bfe7f64e1e", size = 276085, upload-time = "2025-12-30T18:44:09.021Z" }, + { url = "https://files.pythonhosted.org/packages/88/6d/e43452347630efe8133f5304127539100d937c138c0996d27ec63963ec2c/cbor2-5.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:b51c5e59becae746ca4de2bbaa8a2f5c64a68fec05cea62941b1a84a8335f7d1", size = 71657, upload-time = "2025-12-30T18:44:10.162Z" }, + { url = "https://files.pythonhosted.org/packages/8b/66/9a780ef34ab10a0437666232e885378cdd5f60197b1b5e61a62499e5a10a/cbor2-5.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:53b630f4db4b9f477ad84077283dd17ecf9894738aa17ef4938c369958e02a71", size = 67171, upload-time = "2025-12-30T18:44:11.619Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4f/101071f880b4da05771128c0b89f41e334cff044dee05fb013c8f4be661c/cbor2-5.8.0-py3-none-any.whl", hash = "sha256:3727d80f539567b03a7aa11890e57798c67092c38df9e6c23abb059e0f65069c", size = 24374, upload-time = "2025-12-30T18:44:21.476Z" }, ] [[package]] @@ -797,6 +944,18 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, @@ -848,6 +1007,17 @@ version = "3.4.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, @@ -924,32 +1094,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e2/98/0d791b3d1eaed89d7d370b5cf9b8079b124da0545559417f394ba21b5532/colorful-0.5.7-py2.py3-none-any.whl", hash = "sha256:495dd3a23151a9568cee8a90fc1174c902ad7ef06655f50b6bddf9e80008da69", size = 201475, upload-time = "2025-06-30T15:24:02.693Z" }, ] -[[package]] -name = "comet-ml" -version = "3.57.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dulwich" }, - { name = "everett", extra = ["ini"], marker = "extra == 'extra-7-nemo-rl-mcore' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "jsonschema" }, - { name = "psutil" }, - { name = "python-box" }, - { name = "requests" }, - { name = "requests-toolbelt" }, - { name = "rich" }, - { name = "semantic-version" }, - { name = "sentry-sdk" }, - { name = "setuptools" }, - { name = "simplejson" }, - { name = "urllib3" }, - { name = "wrapt" }, - { name = "wurlitzer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b8/e3/39e6a2ecb0f83bacc414c23cbab86a330540396607fc6894588a1d71e092/comet_ml-3.57.3.tar.gz", hash = "sha256:84be2337366d598e8a299eb56e2efcffb859d7de7957148f81f124d3ef02728d", size = 585296, upload-time = "2026-03-12T14:23:22.371Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/17/c875fe4d53286f8664d3edf7185b488cddfca6c208ba978aa34550e9d10d/comet_ml-3.57.3-py3-none-any.whl", hash = "sha256:4eedf5da98a000466759adbd7e96d6f44004ce1bd844e9bc03d6ec5f86583249", size = 786246, upload-time = "2026-03-12T14:23:20.942Z" }, -] - [[package]] name = "compressed-tensors" version = "0.13.0" @@ -957,8 +1101,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "loguru" }, { name = "pydantic" }, - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "transformers" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/65/88dd1c58fb9d0ded51b5c86471b937a1525f91fad2211a6f051dc1ea822d/compressed_tensors-0.13.0.tar.gz", hash = "sha256:23893824d3498ea3f1a829f14a8fa85f9a5e76a34c711a038b8d7c619ca9a67c", size = 200995, upload-time = "2025-12-16T16:03:55.397Z" } @@ -966,25 +1110,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/b5/61ac2563c62490922b603c09113a083fd74af3630ec3931e769484d6dcb5/compressed_tensors-0.13.0-py3-none-any.whl", hash = "sha256:3518799c9baf034eb642efb551db6b0537b8713d45a64fe4def26f7f8d6cabec", size = 192620, upload-time = "2025-12-16T16:03:53.041Z" }, ] -[[package]] -name = "configobj" -version = "5.0.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/c4/c7f9e41bc2e5f8eeae4a08a01c91b2aea3dfab40a3e14b25e87e7db8d501/configobj-5.0.9.tar.gz", hash = "sha256:03c881bbf23aa07bccf1b837005975993c4ab4427ba57f959afdd9d1a2386848", size = 101518, upload-time = "2024-09-21T12:47:46.315Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/c4/0679472c60052c27efa612b4cd3ddd2a23e885dcdc73461781d2c802d39e/configobj-5.0.9-py2.py3-none-any.whl", hash = "sha256:1ba10c5b6ee16229c79a05047aeda2b55eb4e80d7c7d8ecf17ec1ca600c79882", size = 35615, upload-time = "2024-11-26T14:03:32.972Z" }, -] - [[package]] name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, @@ -1037,6 +1183,17 @@ version = "7.10.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/4e/08b493f1f1d8a5182df0044acc970799b58a8d289608e0d891a03e9d269a/coverage-7.10.4.tar.gz", hash = "sha256:25f5130af6c8e7297fd14634955ba9e1697f47143f289e2a23284177c0061d27", size = 823798, upload-time = "2025-08-17T00:26:43.314Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/4a/781c9e4dd57cabda2a28e2ce5b00b6be416015265851060945a5ed4bd85e/coverage-7.10.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a1f0264abcabd4853d4cb9b3d164adbf1565da7dab1da1669e93f3ea60162d79", size = 216706, upload-time = "2025-08-17T00:24:51.528Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8c/51255202ca03d2e7b664770289f80db6f47b05138e06cce112b3957d5dfd/coverage-7.10.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:536cbe6b118a4df231b11af3e0f974a72a095182ff8ec5f4868c931e8043ef3e", size = 216939, upload-time = "2025-08-17T00:24:53.171Z" }, + { url = "https://files.pythonhosted.org/packages/06/7f/df11131483698660f94d3c847dc76461369782d7a7644fcd72ac90da8fd0/coverage-7.10.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9a4c0d84134797b7bf3f080599d0cd501471f6c98b715405166860d79cfaa97e", size = 248429, upload-time = "2025-08-17T00:24:54.934Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fa/13ac5eda7300e160bf98f082e75f5c5b4189bf3a883dd1ee42dbedfdc617/coverage-7.10.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c155fc0f9cee8c9803ea0ad153ab6a3b956baa5d4cd993405dc0b45b2a0b9e0", size = 251178, upload-time = "2025-08-17T00:24:56.353Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/f63b56a58ad0bec68a840e7be6b7ed9d6f6288d790760647bb88f5fea41e/coverage-7.10.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5f2ab6e451d4b07855d8bcf063adf11e199bff421a4ba57f5bb95b7444ca62", size = 252313, upload-time = "2025-08-17T00:24:57.692Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b6/79338f1ea27b01266f845afb4485976211264ab92407d1c307babe3592a7/coverage-7.10.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:685b67d99b945b0c221be0780c336b303a7753b3e0ec0d618c795aada25d5e7a", size = 250230, upload-time = "2025-08-17T00:24:59.293Z" }, + { url = "https://files.pythonhosted.org/packages/bc/93/3b24f1da3e0286a4dc5832427e1d448d5296f8287464b1ff4a222abeeeb5/coverage-7.10.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0c079027e50c2ae44da51c2e294596cbc9dbb58f7ca45b30651c7e411060fc23", size = 248351, upload-time = "2025-08-17T00:25:00.676Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/d59412f869e49dcc5b89398ef3146c8bfaec870b179cc344d27932e0554b/coverage-7.10.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3749aa72b93ce516f77cf5034d8e3c0dfd45c6e8a163a602ede2dc5f9a0bb927", size = 249788, upload-time = "2025-08-17T00:25:02.354Z" }, + { url = "https://files.pythonhosted.org/packages/cc/52/04a3b733f40a0cc7c4a5b9b010844111dbf906df3e868b13e1ce7b39ac31/coverage-7.10.4-cp312-cp312-win32.whl", hash = "sha256:fecb97b3a52fa9bcd5a7375e72fae209088faf671d39fae67261f37772d5559a", size = 219131, upload-time = "2025-08-17T00:25:03.79Z" }, + { url = "https://files.pythonhosted.org/packages/83/dd/12909fc0b83888197b3ec43a4ac7753589591c08d00d9deda4158df2734e/coverage-7.10.4-cp312-cp312-win_amd64.whl", hash = "sha256:26de58f355626628a21fe6a70e1e1fad95702dafebfb0685280962ae1449f17b", size = 219939, upload-time = "2025-08-17T00:25:05.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/c7/058bb3220fdd6821bada9685eadac2940429ab3c97025ce53549ff423cc1/coverage-7.10.4-cp312-cp312-win_arm64.whl", hash = "sha256:67e8885408f8325198862bc487038a4980c9277d753cb8812510927f2176437a", size = 218572, upload-time = "2025-08-17T00:25:06.897Z" }, { url = "https://files.pythonhosted.org/packages/46/b0/4a3662de81f2ed792a4e425d59c4ae50d8dd1d844de252838c200beed65a/coverage-7.10.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b8e1d2015d5dfdbf964ecef12944c0c8c55b885bb5c0467ae8ef55e0e151233", size = 216735, upload-time = "2025-08-17T00:25:08.617Z" }, { url = "https://files.pythonhosted.org/packages/c5/e8/e2dcffea01921bfffc6170fb4406cffb763a3b43a047bbd7923566708193/coverage-7.10.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:25735c299439018d66eb2dccf54f625aceb78645687a05f9f848f6e6c751e169", size = 216982, upload-time = "2025-08-17T00:25:10.384Z" }, { url = "https://files.pythonhosted.org/packages/9d/59/cc89bb6ac869704d2781c2f5f7957d07097c77da0e8fdd4fd50dbf2ac9c0/coverage-7.10.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:715c06cb5eceac4d9b7cdf783ce04aa495f6aff657543fea75c30215b28ddb74", size = 247981, upload-time = "2025-08-17T00:25:11.854Z" }, @@ -1086,55 +1243,58 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.6" +version = "46.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, - { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, - { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, - { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, - { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, - { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, - { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, - { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, - { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, - { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" }, - { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" }, - { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" }, - { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" }, - { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" }, - { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" }, - { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" }, - { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" }, - { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" }, - { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" }, - { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" }, - { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" }, - { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, - { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, - { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, - { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, - { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, - { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, - { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, - { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" }, - { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, ] [[package]] @@ -1142,40 +1302,54 @@ name = "cuda-bindings" version = "12.9.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", ] dependencies = [ { name = "cuda-pathfinder", marker = "sys_platform != 'darwin' or extra == 'extra-7-nemo-rl-sglang' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c2/65bfd79292b8ff18be4dd7f7442cea37bcbc1a228c1886f1dea515c45b67/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:694ba35023846625ef471257e6b5a4bc8af690f961d197d77d34b1d1db393f56", size = 11760260, upload-time = "2025-10-21T14:51:40.79Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/df/6b/9c1b1a6c01392bfdd758e9486f52a1a72bc8f49e98f9355774ef98b5fb4e/cuda_bindings-12.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:696ca75d249ddf287d01b9a698b8e2d8a05046495a9c051ca15659dc52d17615", size = 11586961, upload-time = "2025-10-21T14:51:45.394Z" }, { url = "https://files.pythonhosted.org/packages/05/8b/b4b2d1c7775fa403b64333e720cfcfccef8dcb9cdeb99947061ca5a77628/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf8bfaedc238f3b115d957d1fd6562b7e8435ba57f6d0e2f87d0e7149ccb2da5", size = 11570071, upload-time = "2025-10-21T14:51:47.472Z" }, { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, { url = "https://files.pythonhosted.org/packages/05/d0/d0e4e2e047d8e899f023fa15ad5e9894ce951253f4c894f1cd68490fdb14/cuda_bindings-12.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:a2e82c8985948f953c2be51df45c3fe11c812a928fca525154fb9503190b3e64", size = 11556719, upload-time = "2025-10-21T14:51:52.248Z" }, @@ -1195,13 +1369,16 @@ name = "cuda-bindings" version = "13.0.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "platform_machine != 's390x' and sys_platform == 'darwin'", - "platform_machine == 's390x' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version < '3.13' and sys_platform == 'darwin'", ] dependencies = [ { name = "cuda-pathfinder", marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-fsdp') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/85/b5/e90add0eb01d1ceaaae38c944c8a968090eb25dfbe3c81f5300e39c71739/cuda_bindings-13.0.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a06268a4226c867a7234f12ca183e186e7962a4971b53983c8de182dd62878a3", size = 11929946, upload-time = "2025-08-18T15:29:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/71/2e/51f77c396bb54128a63da74e299edf2c6c4c08ebfb15d48e43665b5fe3b3/cuda_bindings-13.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12dd61b782b1558ac3e3790a02e3d9dc4827c6702a3315a9b79b5e1f6bed30f2", size = 12302099, upload-time = "2025-08-18T15:29:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/f0/84/e1ccf4e52d60da76ae538f86c6e73425ae1dc226b4a528893ea2012e0646/cuda_bindings-13.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:06b4533e43c65bf2422db25eb86cd0813a818a2e3cb4b793f4afbdb2f801d894", size = 12046683, upload-time = "2025-08-18T15:29:41.267Z" }, { url = "https://files.pythonhosted.org/packages/b5/8a/4112bc04f110a89d751b3d580189debe64240c94f6609b6d9e1bd16db16b/cuda_bindings-13.0.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a38090fa452f71c30b93b9007c26f1874c28abc5662b37c5caefbd41506a64f7", size = 11937804, upload-time = "2025-08-18T15:29:43.262Z" }, { url = "https://files.pythonhosted.org/packages/53/43/c3aa3637458edd10014cf16a4152faca17d8fb6cc233fec23d469eb042aa/cuda_bindings-13.0.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bdea32e9ede085c72dbc670d2c6ecd68451aa06d0f1cfe597374483dcdd1657", size = 12316943, upload-time = "2025-08-18T15:29:45.261Z" }, { url = "https://files.pythonhosted.org/packages/ed/fd/033f669fdda93bab7c342d9e08b8bd97b9a3670bd8f5ee5dbc51054d54db/cuda_bindings-13.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:448bf908d17b29e3c5dfa55f848e37f3d4170e5c8644536323bf54a0785e6b98", size = 12009034, upload-time = "2025-08-18T15:29:47.343Z" }, @@ -1212,10 +1389,13 @@ name = "cuda-core" version = "0.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/aa/073f74544f7e21d689986ed6367b94ea94f0145995db0d248f5d6936486e/cuda_core-0.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78876c513d484ec94ac08402d9b9059d1d8665ffd8cdc33a23b93424747c3c47", size = 17671566, upload-time = "2026-01-15T15:40:37.898Z" }, + { url = "https://files.pythonhosted.org/packages/9d/01/12392ff1555e2ce77f4d7857c0027aca3c912e936828642206ba51d7149d/cuda_core-0.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82e687b0a5d4fde99d7bb266c60996fc266a55dc00c888eb0d7dba70bb127a09", size = 17887562, upload-time = "2026-01-15T15:40:40.658Z" }, + { url = "https://files.pythonhosted.org/packages/28/97/2d189863d9d4d1f1a00aa1cc325584dc14b0b909132fc672f82c23faafff/cuda_core-0.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f192a0fd39836343158aa4e5b0e8e24280b26debb414ebad2af3794585731988", size = 4441446, upload-time = "2026-01-15T15:40:42.91Z" }, { url = "https://files.pythonhosted.org/packages/7f/41/2cd8225b2d95191b62b0da6ad4248ad5023bba9d23c355e0b3b151c1f21f/cuda_core-0.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c89270e8a332f8c9e18e423d7e1d08d6a82115419ec813f53784d48116fc6fc6", size = 17461993, upload-time = "2026-01-15T15:40:44.796Z" }, { url = "https://files.pythonhosted.org/packages/24/66/52f200a80f33c4a6a3c5da282fb2167192e20c8bc1fa4dbf33602d63be8f/cuda_core-0.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbfbdd753d58609c6b882bce5cbb6e341a338a150603b3849039a96641474b1d", size = 17689097, upload-time = "2026-01-15T15:40:47.486Z" }, { url = "https://files.pythonhosted.org/packages/41/a8/1bfeafa05974532e50086f9d56ffeb7742b0be8a827a63fb94c309b40b9f/cuda_core-0.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:1826a17a06dbc46e0d9ddfb306bd0baac5cf9218b700fe517d380f8113b676e6", size = 4427105, upload-time = "2026-01-15T15:40:49.885Z" }, @@ -1240,35 +1420,46 @@ name = "cuda-python" version = "12.9.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", ] dependencies = [ { name = "cuda-bindings", version = "12.9.4", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin' or extra == 'extra-7-nemo-rl-sglang' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, @@ -1282,8 +1473,8 @@ name = "cuda-python" version = "13.0.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "platform_machine != 's390x' and sys_platform == 'darwin'", - "platform_machine == 's390x' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version < '3.13' and sys_platform == 'darwin'", ] dependencies = [ { name = "cuda-bindings", version = "13.0.1", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-fsdp') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, @@ -1302,6 +1493,9 @@ dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/12/c5/7e7fc4816d0de0154e5d9053242c3a08a0ca8b43ee656a6f7b3b95055a7b/cupy_cuda12x-13.6.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:a6970ceefe40f9acbede41d7fe17416bd277b1bd2093adcde457b23b578c5a59", size = 127334633, upload-time = "2025-08-18T08:24:43.065Z" }, + { url = "https://files.pythonhosted.org/packages/e0/95/d7e1295141e7d530674a3cc567e13ed0eb6b81524cb122d797ed996b5bea/cupy_cuda12x-13.6.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:79b0cacb5e8b190ef409f9e03f06ac8de1b021b0c0dda47674d446f5557e0eb1", size = 112886268, upload-time = "2025-08-18T08:24:49.294Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/14555b63fd78cfac7b88af0094cea0a3cb845d243661ec7da69f7b3ea0de/cupy_cuda12x-13.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca06fede7b8b83ca9ad80062544ef2e5bb8d4762d1c4fc3ac8349376de9c8a5e", size = 89785108, upload-time = "2025-08-18T08:24:54.527Z" }, { url = "https://files.pythonhosted.org/packages/19/ec/f62cb991f11fb41291c4c15b6936d7b67ffa71ddb344ad6e8894e06ce58d/cupy_cuda12x-13.6.0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e5426ae3b1b9cf59927481e457a89e3f0b50a35b114a8034ec9110e7a833434c", size = 126904601, upload-time = "2025-08-18T08:24:59.951Z" }, { url = "https://files.pythonhosted.org/packages/f8/b8/30127bcdac53a25f94ee201bf4802fcd8d012145567d77c54174d6d01c01/cupy_cuda12x-13.6.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:52d9e7f83d920da7d81ec2e791c2c2c747fdaa1d7b811971b34865ce6371e98a", size = 112654824, upload-time = "2025-08-18T08:25:05.944Z" }, { url = "https://files.pythonhosted.org/packages/72/36/c9e24acb19f039f814faea880b3704a3661edaa6739456b73b27540663e3/cupy_cuda12x-13.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:297b4268f839de67ef7865c2202d3f5a0fb8d20bd43360bc51b6e60cb4406447", size = 89750580, upload-time = "2025-08-18T08:25:10.972Z" }, @@ -1340,8 +1534,8 @@ dependencies = [ { name = "httpx" }, { name = "huggingface-hub" }, { name = "multiprocess" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "packaging" }, { name = "pandas" }, { name = "pyarrow" }, @@ -1361,6 +1555,10 @@ version = "1.8.16" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/d4/722d0bcc7986172ac2ef3c979ad56a1030e3afd44ced136d45f8142b1f4a/debugpy-1.8.16.tar.gz", hash = "sha256:31e69a1feb1cf6b51efbed3f6c9b0ef03bc46ff050679c4be7ea6d2e23540870", size = 1643809, upload-time = "2025-08-06T18:00:02.647Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/61/fb/0387c0e108d842c902801bc65ccc53e5b91d8c169702a9bbf4f7efcedf0c/debugpy-1.8.16-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:b202e2843e32e80b3b584bcebfe0e65e0392920dc70df11b2bfe1afcb7a085e4", size = 2511822, upload-time = "2025-08-06T18:00:18.526Z" }, + { url = "https://files.pythonhosted.org/packages/37/44/19e02745cae22bf96440141f94e15a69a1afaa3a64ddfc38004668fcdebf/debugpy-1.8.16-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64473c4a306ba11a99fe0bb14622ba4fbd943eb004847d9b69b107bde45aa9ea", size = 4230135, upload-time = "2025-08-06T18:00:19.997Z" }, + { url = "https://files.pythonhosted.org/packages/f3/0b/19b1ba5ee4412f303475a2c7ad5858efb99c90eae5ec627aa6275c439957/debugpy-1.8.16-cp312-cp312-win32.whl", hash = "sha256:833a61ed446426e38b0dd8be3e9d45ae285d424f5bf6cd5b2b559c8f12305508", size = 5281271, upload-time = "2025-08-06T18:00:21.281Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e0/bc62e2dc141de53bd03e2c7cb9d7011de2e65e8bdcdaa26703e4d28656ba/debugpy-1.8.16-cp312-cp312-win_amd64.whl", hash = "sha256:75f204684581e9ef3dc2f67687c3c8c183fde2d6675ab131d94084baf8084121", size = 5323149, upload-time = "2025-08-06T18:00:23.033Z" }, { url = "https://files.pythonhosted.org/packages/62/66/607ab45cc79e60624df386e233ab64a6d8d39ea02e7f80e19c1d451345bb/debugpy-1.8.16-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:85df3adb1de5258dca910ae0bb185e48c98801ec15018a263a92bb06be1c8787", size = 2496157, upload-time = "2025-08-06T18:00:24.361Z" }, { url = "https://files.pythonhosted.org/packages/4d/a0/c95baae08a75bceabb79868d663a0736655e427ab9c81fb848da29edaeac/debugpy-1.8.16-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee89e948bc236a5c43c4214ac62d28b29388453f5fd328d739035e205365f0b", size = 4222491, upload-time = "2025-08-06T18:00:25.806Z" }, { url = "https://files.pythonhosted.org/packages/5b/2f/1c8db6ddd8a257c3cd2c46413b267f1d5fa3df910401c899513ce30392d6/debugpy-1.8.16-cp313-cp313-win32.whl", hash = "sha256:cf358066650439847ec5ff3dae1da98b5461ea5da0173d93d5e10f477c94609a", size = 5281126, upload-time = "2025-08-06T18:00:27.207Z" }, @@ -1382,10 +1580,13 @@ name = "decord2" version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/7b/acd54cde40c18025aaba0a2e8e076d5782beb5d4997360ed2aeb4cab22a9/decord2-3.0.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cd7a7ad98b5ee26a19c4827e9bd2e8dc4b6afb8344f15ff308c519fd5196d949", size = 20360404, upload-time = "2025-12-18T14:39:00.977Z" }, + { url = "https://files.pythonhosted.org/packages/70/5d/9922f076649e7dbb2c14e47ecdcac1422ead2cd858a002451665d6e0517b/decord2-3.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fd8474e1f65f12447b1e69106f13eed805aa050be301751268018332839416cd", size = 28662589, upload-time = "2025-12-18T14:39:03.323Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6f/8d9cf20aac657cb0b31892865c524402d3117a5da37a24007c4833c52a57/decord2-3.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:96a429c725fce26fe230b29cffa9507d30b2a4b1af6c99b411b58597afa0eb72", size = 30123769, upload-time = "2025-12-18T14:39:05.933Z" }, { url = "https://files.pythonhosted.org/packages/83/97/7aa76800bb80d647215dcf5f471e147f26437ce70c60f01919b03b1583f1/decord2-3.0.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:81b03239fa891dd69ce3796a2095c81ab4bfc483abe2e13934999eb08c4c9e7f", size = 20360404, upload-time = "2025-12-18T14:39:08.422Z" }, { url = "https://files.pythonhosted.org/packages/f8/d2/a3b28cc4d914ec2aa893639c85a082450b455b0244a33a3e42fc66255317/decord2-3.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:44fa35c687e873adf7dcdc32bd00cacb27143bca7ef8c3cdd2cfeea2fc8a4d1a", size = 28662588, upload-time = "2025-12-18T14:39:10.717Z" }, { url = "https://files.pythonhosted.org/packages/53/6e/7ae997c25c200efcfb5f38af58739d86aa15f9b6ac8ff0edc2ad977d30f4/decord2-3.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:331b69892d594bc1177ac2f2eda97070ba5eec51ef2814da1d39c6ebba0c1213", size = 30123767, upload-time = "2025-12-18T14:39:13.356Z" }, @@ -1407,8 +1608,8 @@ source = { git = "https://github.com/deepseek-ai/DeepEP.git?rev=bfded34800dfec41 dependencies = [ { name = "ninja" }, { name = "packaging" }, - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-fsdp') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-automodel') or (sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-fsdp') or (sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang')" }, + { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-automodel') or (sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] [[package]] @@ -1418,8 +1619,8 @@ source = { git = "https://github.com/deepseek-ai/DeepGEMM.git?rev=7b6b5563b9d4c1 dependencies = [ { name = "ninja" }, { name = "packaging" }, - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] [[package]] @@ -1470,26 +1671,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/ae/afb1487556e2dc827a17097aac8158a25b433a345386f0e249f6d2694ccb/devtools-0.12.2-py3-none-any.whl", hash = "sha256:c366e3de1df4cdd635f1ad8cbcd3af01a384d7abda71900e68d43b04eb6aaca7", size = 19411, upload-time = "2023-09-03T16:56:59.049Z" }, ] -[[package]] -name = "diffusers" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "httpx" }, - { name = "huggingface-hub" }, - { name = "importlib-metadata" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" } }, - { name = "pillow" }, - { name = "regex" }, - { name = "requests" }, - { name = "safetensors" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/58/3b/01d0ff800b811c5ad8bba682f4c6abf1d7071cd81464c01724333fefb7ba/diffusers-0.37.0.tar.gz", hash = "sha256:408789af73898585f525afd07ca72b3955affea4216a669558e9f59b5b1fe704", size = 4141136, upload-time = "2026-03-05T14:58:39.704Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/55/586a3a2b9c95f371c9c3cb048c3cac15aedcce8d6d53ebd6bbc46860722d/diffusers-0.37.0-py3-none-any.whl", hash = "sha256:7eab74bf896974250b5e1027cae813aba1004f02d97c9b44891b83713386aa08", size = 5000449, upload-time = "2026-03-05T14:58:37.361Z" }, -] - [[package]] name = "dill" version = "0.3.8" @@ -1574,34 +1755,12 @@ wheels = [ ] [[package]] -name = "drain3" -version = "0.9.11" +name = "durationpy" +version = "0.10" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cachetools" }, - { name = "jsonpickle" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/83/4da2d3a11b5e0edf1a4f4c0c2dd42126d2eb1f31c733967edd3dfac1af94/drain3-0.9.11.tar.gz", hash = "sha256:9ab4b1407fad74f56554ae371ef019c3c7985861631f4bab46a0e92585125f75", size = 27960, upload-time = "2022-07-17T06:40:11.433Z" } - -[[package]] -name = "dulwich" -version = "0.25.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/9c9bc6ac66007f8090b1da9079c0e4bbea5aa9583c3c12098e0f11462dd5/dulwich-0.25.2.tar.gz", hash = "sha256:bca22c8aa4cbecbe8493b76e3fd6101513f09cf405cd9b92e116a48d9469e55a", size = 1126499, upload-time = "2026-01-11T22:04:47.667Z" } +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/82/6b/a2f422be19ddbbd6a56477e0a40a8ea7c58628467e655143c249d8c320cf/dulwich-0.25.2-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:46bfb777b33f2906c9800ce8c8ad0ea0530c1c2d1145eab6d42c40de29f73efa", size = 1419859, upload-time = "2026-01-11T22:04:30.721Z" }, - { url = "https://files.pythonhosted.org/packages/2e/ee/d0954d64322955d8cd1c482263925ca75378e640851218cb14ffe16aae07/dulwich-0.25.2-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a845afcd30d049a222240f9efdec6b95c2b6fd839564777061e6209e54c3ffc", size = 1419852, upload-time = "2026-01-11T22:04:32.669Z" }, - { url = "https://files.pythonhosted.org/packages/4e/cf/07f6a26837e79b5f6483fdc77f79f661aa59ed86fcc13e61bc233d95e6d4/dulwich-0.25.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:26bfe8c35680dd0cf71ce724e0f00401a439a332e8bd90a82e556ab2cb3a68e6", size = 1318305, upload-time = "2026-01-11T22:04:34.142Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2a/aa784b51554d005a35ff78859424e9b69e9c4124533e5063ebe4161ad10c/dulwich-0.25.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e7ec5bc1e769b19312d1ae431981096aa046925e9cb278b8efff6bebdb679b12", size = 1394619, upload-time = "2026-01-11T22:04:35.832Z" }, - { url = "https://files.pythonhosted.org/packages/89/93/4e95a9a92fbc01f5d1bf996b6393c3dabde26031c1c8100355c189fec8f4/dulwich-0.25.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ab15cc01c19bb1b258f6843470637bc5f2d886b8244bb48f8da8ee3d766bcf10", size = 1422512, upload-time = "2026-01-11T22:04:37.481Z" }, - { url = "https://files.pythonhosted.org/packages/c4/7e/d7b1b0c83457e2ad75cee64e1390151ac25ac89597e5a8f6530137e1c1fd/dulwich-0.25.2-cp313-cp313-win32.whl", hash = "sha256:a7ccd96e3beb93df7458191f0aadad6e76ab78f09452f867fc06cd4f99423c7e", size = 983597, upload-time = "2026-01-11T22:04:39.064Z" }, - { url = "https://files.pythonhosted.org/packages/1a/4a/3cb5178b49a8be5d311276af33a8e6f8d3cce0f6410b6c03ab99b96e74eb/dulwich-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f84e6501702877ecc1c1a8710c745942d86d2f55cbfeaf99377100e4c16139a", size = 1000141, upload-time = "2026-01-11T22:04:40.604Z" }, - { url = "https://files.pythonhosted.org/packages/82/ec/494f14d73346309e2e03fdd1fa82618d91bbc59423bbe8a6f6a7b20186ee/dulwich-0.25.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:b1b54442dd8171fc5a1e0d5efc7d72b8192c88f738ee9d72e7aa82bf9d630832", size = 1437740, upload-time = "2026-01-11T22:04:42.297Z" }, - { url = "https://files.pythonhosted.org/packages/c8/48/8448a48054f61e1c4c7c42f2ab29cdb576451545d2843651f69802ff15fb/dulwich-0.25.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:0ac0b70a970fac9b9c161ce2f1472915656c91e8fdb2dcfb1b5f84e6a127a184", size = 1437733, upload-time = "2026-01-11T22:04:43.978Z" }, - { url = "https://files.pythonhosted.org/packages/87/eb/153b2b32dca090e956a1e512293db3c7c144db50da439373d1be56880512/dulwich-0.25.2-py3-none-any.whl", hash = "sha256:19dd5a0e08a47483be7f404e2555136a9ebaf70781fee3280457f8e2d65b2388", size = 650045, upload-time = "2026-01-11T22:04:45.398Z" }, + { 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]] @@ -1637,26 +1796,13 @@ wheels = [ [[package]] name = "emerging-optimizers" -version = "0.2.0" -source = { git = "https://github.com/NVIDIA-NeMo/Emerging-Optimizers.git?rev=v0.2.0#1effa026ff096b7fa1063ca2fba19d98be6e6cdf" } +version = "0.1.0" +source = { git = "https://github.com/NVIDIA-NeMo/Emerging-Optimizers.git?rev=v0.1.0#d5363b4a418128cd8111983b191c4b8869a9766b" } dependencies = [ { name = "absl-py" }, { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "sys_platform != 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, -] - -[[package]] -name = "everett" -version = "3.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/b4/c7c61c0b243c4277d19299cd1bccee8b2b57d04073c0d8625799fe47f5c9/everett-3.1.0.tar.gz", hash = "sha256:46175da5bcb06c193aa129e59714bca981344ff067c3a8bc2e625bc0b3dc01f6", size = 73796, upload-time = "2022-10-26T15:15:00.651Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/9a/d882fd7562208456236fb2e62b762bf16fbc9ecde842bb871f676ca0f7e1/everett-3.1.0-py2.py3-none-any.whl", hash = "sha256:db13891b849e45e54faea93ee79881d12458c5378f5b9b7f806eeff03ce1de3c", size = 35702, upload-time = "2022-10-26T15:14:58.698Z" }, -] - -[package.optional-dependencies] -ini = [ - { name = "configobj" }, + { name = "typing-extensions" }, ] [[package]] @@ -1695,11 +1841,11 @@ wheels = [ [package.optional-dependencies] standard = [ { name = "email-validator" }, - { name = "fastapi-cli", extra = ["standard"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "fastapi-cli", extra = ["standard"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "httpx" }, { name = "jinja2" }, { name = "python-multipart" }, - { name = "uvicorn", extra = ["standard"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "uvicorn", extra = ["standard"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] [[package]] @@ -1709,7 +1855,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "rich-toolkit" }, { name = "typer" }, - { name = "uvicorn", extra = ["standard"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "uvicorn", extra = ["standard"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c6/94/3ef75d9c7c32936ecb539b9750ccbdc3d2568efd73b1cb913278375f4533/fastapi_cli-0.0.8.tar.gz", hash = "sha256:2360f2989b1ab4a3d7fc8b3a0b20e8288680d8af2e31de7c38309934d7f8a0ee", size = 16884, upload-time = "2025-07-07T14:44:09.326Z" } wheels = [ @@ -1719,7 +1865,7 @@ wheels = [ [package.optional-dependencies] standard = [ { name = "fastapi-cloud-cli" }, - { name = "uvicorn", extra = ["standard"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "uvicorn", extra = ["standard"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] [[package]] @@ -1728,12 +1874,12 @@ version = "0.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, - { name = "pydantic", extra = ["email"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "pydantic", extra = ["email"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "rich-toolkit" }, { name = "rignore" }, { name = "sentry-sdk" }, { name = "typer" }, - { name = "uvicorn", extra = ["standard"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "uvicorn", extra = ["standard"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/2e/3b6e5016affc310e5109bc580f760586eabecea0c8a7ab067611cd849ac0/fastapi_cloud_cli-0.1.5.tar.gz", hash = "sha256:341ee585eb731a6d3c3656cb91ad38e5f39809bf1a16d41de1333e38635a7937", size = 22710, upload-time = "2025-07-28T13:30:48.216Z" } wheels = [ @@ -1746,6 +1892,12 @@ version = "0.8.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/73/b1/1c3d635d955f2b4bf34d45abf8f35492e04dbd7804e94ce65d9f928ef3ec/fastrlock-0.8.3.tar.gz", hash = "sha256:4af6734d92eaa3ab4373e6c9a1dd0d5ad1304e172b1521733c6c3b3d73c8fa5d", size = 79327, upload-time = "2024-12-17T11:03:39.638Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/00/df/56270f2e10c1428855c990e7a7e5baafa9e1262b8e789200bd1d047eb501/fastrlock-0.8.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:8cb2cf04352ea8575d496f31b3b88c42c7976e8e58cdd7d1550dfba80ca039da", size = 55727, upload-time = "2024-12-17T11:02:17.26Z" }, + { url = "https://files.pythonhosted.org/packages/57/21/ea1511b0ef0d5457efca3bf1823effb9c5cad4fc9dca86ce08e4d65330ce/fastrlock-0.8.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85a49a1f1e020097d087e1963e42cea6f307897d5ebe2cb6daf4af47ffdd3eed", size = 52201, upload-time = "2024-12-17T11:02:19.512Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/cdecb7aa976f34328372f1c4efd6c9dc1b039b3cc8d3f38787d640009a25/fastrlock-0.8.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f13ec08f1adb1aa916c384b05ecb7dbebb8df9ea81abd045f60941c6283a670", size = 53924, upload-time = "2024-12-17T11:02:20.85Z" }, + { url = "https://files.pythonhosted.org/packages/88/6d/59c497f8db9a125066dd3a7442fab6aecbe90d6fec344c54645eaf311666/fastrlock-0.8.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0ea4e53a04980d646def0f5e4b5e8bd8c7884288464acab0b37ca0c65c482bfe", size = 52140, upload-time = "2024-12-17T11:02:22.263Z" }, + { url = "https://files.pythonhosted.org/packages/62/04/9138943c2ee803d62a48a3c17b69de2f6fa27677a6896c300369e839a550/fastrlock-0.8.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:38340f6635bd4ee2a4fb02a3a725759fe921f2ca846cb9ca44531ba739cc17b4", size = 53261, upload-time = "2024-12-17T11:02:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4b/db35a52589764c7745a613b6943bbd018f128d42177ab92ee7dde88444f6/fastrlock-0.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:da06d43e1625e2ffddd303edcd6d2cd068e1c486f5fd0102b3f079c44eb13e2c", size = 31235, upload-time = "2024-12-17T11:02:25.708Z" }, { url = "https://files.pythonhosted.org/packages/92/74/7b13d836c3f221cff69d6f418f46c2a30c4b1fe09a8ce7db02eecb593185/fastrlock-0.8.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5264088185ca8e6bc83181dff521eee94d078c269c7d557cc8d9ed5952b7be45", size = 54157, upload-time = "2024-12-17T11:02:29.196Z" }, { url = "https://files.pythonhosted.org/packages/06/77/f06a907f9a07d26d0cca24a4385944cfe70d549a2c9f1c3e3217332f4f12/fastrlock-0.8.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a98ba46b3e14927550c4baa36b752d0d2f7387b8534864a8767f83cce75c160", size = 50954, upload-time = "2024-12-17T11:02:32.12Z" }, { url = "https://files.pythonhosted.org/packages/f9/4e/94480fb3fd93991dd6f4e658b77698edc343f57caa2870d77b38c89c2e3b/fastrlock-0.8.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbdea6deeccea1917c6017d353987231c4e46c93d5338ca3e66d6cd88fbce259", size = 52535, upload-time = "2024-12-17T11:02:33.402Z" }, @@ -1754,6 +1906,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/a3/2ad0a0a69662fd4cf556ab8074f0de978ee9b56bff6ddb4e656df4aa9e8e/fastrlock-0.8.3-cp313-cp313-win_amd64.whl", hash = "sha256:8d1d6a28291b4ace2a66bd7b49a9ed9c762467617febdd9ab356b867ed901af8", size = 30472, upload-time = "2024-12-17T11:02:37.983Z" }, ] +[[package]] +name = "ffmpy" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/11/d1cf103779efaf9fd165b4ef5f7b8f44c4e692bb44638082d3980f83afc8/ffmpy-0.6.2.tar.gz", hash = "sha256:24fdb5d6476e7d73e6d5aaa95a6c34f66a214fb0f905d459609b3ba452dd940b", size = 4958, upload-time = "2025-10-08T06:34:50.063Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/57/699c8b55080248027b97a910818de7ce32e87e2dd60de5e81cff3a353745/ffmpy-0.6.2-py3-none-any.whl", hash = "sha256:ce6b8582e236a272f39f341dc37e7ddf313cb061eca52f9acf1b60062fe8e2fe", size = 5495, upload-time = "2025-10-08T06:34:48.665Z" }, +] + [[package]] name = "filelock" version = "3.19.1" @@ -1800,25 +1961,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/e8/6d/7066d160bdffa2f9da29a8c3957f266b17a03ca0b3bdc8fdae86d9881fe7/flash_attn-2.8.1.tar.gz", hash = "sha256:0ff003899fcb244f357905b04f622d5c9736887126dd6675f8f4bc52954e3923", size = 8166563, upload-time = "2025-07-10T05:16:39.729Z" } -[[package]] -name = "flash-attn-4" -version = "4.0.0b8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "apache-tvm-ffi" }, - { name = "einops" }, - { name = "nvidia-cutlass-dsl" }, - { name = "quack-kernels" }, - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torch-c-dlpack-ext" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6a/3d/22e3a3f17a1d0b405c2f08788353c074b173af1ae359985d207826bff0bc/flash_attn_4-4.0.0b8.tar.gz", hash = "sha256:c1007ded9bfdb214715c56cdb35cf3eee096a7ec877f4963f00f1244c98d35ff", size = 213740, upload-time = "2026-04-08T08:32:55.196Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/15/a5bfe1e92c868331a16747543f1b937074a6b398caf630c4813336d70418/flash_attn_4-4.0.0b8-py3-none-any.whl", hash = "sha256:bae222f60169fb411ce1fdd0d2e0f43f313d25f9f370c40a8b0edaf2743b3b62", size = 231322, upload-time = "2026-04-08T08:32:53.835Z" }, -] - [[package]] name = "flash-linear-attention" version = "0.4.1" @@ -1849,8 +1991,8 @@ dependencies = [ { name = "click" }, { name = "einops" }, { name = "ninja" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "nvidia-cudnn-frontend" }, { name = "nvidia-cutlass-dsl" }, { name = "nvidia-ml-py" }, @@ -1875,8 +2017,7 @@ dependencies = [ { name = "packaging" }, { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-automodel') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "triton", version = "3.6.0", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(platform_machine != 's390x' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel') or (platform_machine == 's390x' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (platform_machine == 's390x' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (platform_machine == 's390x' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine == 's390x' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 's390x' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "triton", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/97/f5/3f463bff2d9f86c230c0ef5c7632ae180e25e4523a4494ad8844ed704879/flashoptim-0.1.3.tar.gz", hash = "sha256:8dc7238224883aa3e8aca40f992d7432e76d5c641255a9dd29c5633741fbfdf1", size = 90240, upload-time = "2026-03-07T09:29:11.414Z" } wheels = [ @@ -1915,35 +2056,43 @@ wheels = [ [[package]] name = "fonttools" -version = "4.62.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, - { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, - { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, - { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, - { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, - { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, - { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, - { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, - { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, - { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, - { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, - { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, - { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, - { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, - { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, - { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, - { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, - { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, - { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, - { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, +version = "4.59.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/7f/29c9c3fe4246f6ad96fee52b88d0dc3a863c7563b0afc959e36d78b965dc/fonttools-4.59.1.tar.gz", hash = "sha256:74995b402ad09822a4c8002438e54940d9f1ecda898d2bb057729d7da983e4cb", size = 3534394, upload-time = "2025-08-14T16:28:14.266Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/fe/6e069cc4cb8881d164a9bd956e9df555bc62d3eb36f6282e43440200009c/fonttools-4.59.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:43ab814bbba5f02a93a152ee61a04182bb5809bd2bc3609f7822e12c53ae2c91", size = 2769172, upload-time = "2025-08-14T16:26:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/b9/98/ec4e03f748fefa0dd72d9d95235aff6fef16601267f4a2340f0e16b9330f/fonttools-4.59.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4f04c3ffbfa0baafcbc550657cf83657034eb63304d27b05cff1653b448ccff6", size = 2337281, upload-time = "2025-08-14T16:26:47.921Z" }, + { url = "https://files.pythonhosted.org/packages/8b/b1/890360a7e3d04a30ba50b267aca2783f4c1364363797e892e78a4f036076/fonttools-4.59.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d601b153e51a5a6221f0d4ec077b6bfc6ac35bfe6c19aeaa233d8990b2b71726", size = 4909215, upload-time = "2025-08-14T16:26:49.682Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ec/2490599550d6c9c97a44c1e36ef4de52d6acf742359eaa385735e30c05c4/fonttools-4.59.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c735e385e30278c54f43a0d056736942023c9043f84ee1021eff9fd616d17693", size = 4951958, upload-time = "2025-08-14T16:26:51.616Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/bd053f6f7634234a9b9805ff8ae4f32df4f2168bee23cafd1271ba9915a9/fonttools-4.59.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1017413cdc8555dce7ee23720da490282ab7ec1cf022af90a241f33f9a49afc4", size = 4894738, upload-time = "2025-08-14T16:26:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a1/3cd12a010d288325a7cfcf298a84825f0f9c29b01dee1baba64edfe89257/fonttools-4.59.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5c6d8d773470a5107052874341ed3c487c16ecd179976d81afed89dea5cd7406", size = 5045983, upload-time = "2025-08-14T16:26:56.153Z" }, + { url = "https://files.pythonhosted.org/packages/a2/af/8a2c3f6619cc43cf87951405337cc8460d08a4e717bb05eaa94b335d11dc/fonttools-4.59.1-cp312-cp312-win32.whl", hash = "sha256:2a2d0d33307f6ad3a2086a95dd607c202ea8852fa9fb52af9b48811154d1428a", size = 2203407, upload-time = "2025-08-14T16:26:58.165Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f2/a19b874ddbd3ebcf11d7e25188ef9ac3f68b9219c62263acb34aca8cde05/fonttools-4.59.1-cp312-cp312-win_amd64.whl", hash = "sha256:0b9e4fa7eaf046ed6ac470f6033d52c052481ff7a6e0a92373d14f556f298dc0", size = 2251561, upload-time = "2025-08-14T16:27:00.646Z" }, + { url = "https://files.pythonhosted.org/packages/19/5e/94a4d7f36c36e82f6a81e0064d148542e0ad3e6cf51fc5461ca128f3658d/fonttools-4.59.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:89d9957b54246c6251345297dddf77a84d2c19df96af30d2de24093bbdf0528b", size = 2760192, upload-time = "2025-08-14T16:27:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a5/f50712fc33ef9d06953c660cefaf8c8fe4b8bc74fa21f44ee5e4f9739439/fonttools-4.59.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8156b11c0d5405810d216f53907bd0f8b982aa5f1e7e3127ab3be1a4062154ff", size = 2332694, upload-time = "2025-08-14T16:27:04.883Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a2/5a9fc21c354bf8613215ce233ab0d933bd17d5ff4c29693636551adbc7b3/fonttools-4.59.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8387876a8011caec52d327d5e5bca705d9399ec4b17afb8b431ec50d47c17d23", size = 4889254, upload-time = "2025-08-14T16:27:07.02Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e5/54a6dc811eba018d022ca2e8bd6f2969291f9586ccf9a22a05fc55f91250/fonttools-4.59.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb13823a74b3a9204a8ed76d3d6d5ec12e64cc5bc44914eb9ff1cdac04facd43", size = 4949109, upload-time = "2025-08-14T16:27:09.3Z" }, + { url = "https://files.pythonhosted.org/packages/db/15/b05c72a248a95bea0fd05fbd95acdf0742945942143fcf961343b7a3663a/fonttools-4.59.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e1ca10da138c300f768bb68e40e5b20b6ecfbd95f91aac4cc15010b6b9d65455", size = 4888428, upload-time = "2025-08-14T16:27:11.514Z" }, + { url = "https://files.pythonhosted.org/packages/63/71/c7d6840f858d695adc0c4371ec45e3fb1c8e060b276ba944e2800495aca4/fonttools-4.59.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2beb5bfc4887a3130f8625349605a3a45fe345655ce6031d1bac11017454b943", size = 5032668, upload-time = "2025-08-14T16:27:13.872Z" }, + { url = "https://files.pythonhosted.org/packages/90/54/57be4aca6f1312e2bc4d811200dd822325794e05bdb26eeff0976edca651/fonttools-4.59.1-cp313-cp313-win32.whl", hash = "sha256:419f16d750d78e6d704bfe97b48bba2f73b15c9418f817d0cb8a9ca87a5b94bf", size = 2201832, upload-time = "2025-08-14T16:27:16.126Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1f/1899a6175a5f900ed8730a0d64f53ca1b596ed7609bfda033cf659114258/fonttools-4.59.1-cp313-cp313-win_amd64.whl", hash = "sha256:c536f8a852e8d3fa71dde1ec03892aee50be59f7154b533f0bf3c1174cfd5126", size = 2250673, upload-time = "2025-08-14T16:27:18.033Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/f6ba82c22f118d9985c37fea65d8d715ca71300d78b6c6e90874dc59f11d/fonttools-4.59.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d5c3bfdc9663f3d4b565f9cb3b8c1efb3e178186435b45105bde7328cfddd7fe", size = 2758606, upload-time = "2025-08-14T16:27:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/3a/81/84aa3d0ce27b0112c28b67b637ff7a47cf401cf5fbfee6476e4bc9777580/fonttools-4.59.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ea03f1da0d722fe3c2278a05957e6550175571a4894fbf9d178ceef4a3783d2b", size = 2330187, upload-time = "2025-08-14T16:27:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/17/41/b3ba43f78afb321e2e50232c87304c8d0f5ab39b64389b8286cc39cdb824/fonttools-4.59.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57a3708ca6bfccb790f585fa6d8f29432ec329618a09ff94c16bcb3c55994643", size = 4832020, upload-time = "2025-08-14T16:27:24.214Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/3af871c7fb325a68938e7ce544ca48bfd2c6bb7b357f3c8252933b29100a/fonttools-4.59.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:729367c91eb1ee84e61a733acc485065a00590618ca31c438e7dd4d600c01486", size = 4930687, upload-time = "2025-08-14T16:27:26.484Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4f/299fc44646b30d9ef03ffaa78b109c7bd32121f0d8f10009ee73ac4514bc/fonttools-4.59.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f8ef66ac6db450193ed150e10b3b45dde7aded10c5d279968bc63368027f62b", size = 4875794, upload-time = "2025-08-14T16:27:28.887Z" }, + { url = "https://files.pythonhosted.org/packages/90/cf/a0a3d763ab58f5f81ceff104ddb662fd9da94248694862b9c6cbd509fdd5/fonttools-4.59.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:075f745d539a998cd92cb84c339a82e53e49114ec62aaea8307c80d3ad3aef3a", size = 4985780, upload-time = "2025-08-14T16:27:30.858Z" }, + { url = "https://files.pythonhosted.org/packages/72/c5/ba76511aaae143d89c29cd32ce30bafb61c477e8759a1590b8483f8065f8/fonttools-4.59.1-cp314-cp314-win32.whl", hash = "sha256:c2b0597522d4c5bb18aa5cf258746a2d4a90f25878cbe865e4d35526abd1b9fc", size = 2205610, upload-time = "2025-08-14T16:27:32.578Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/b250e69d6caf35bc65cddbf608be0662d741c248f2e7503ab01081fc267e/fonttools-4.59.1-cp314-cp314-win_amd64.whl", hash = "sha256:e9ad4ce044e3236f0814c906ccce8647046cc557539661e35211faadf76f283b", size = 2255376, upload-time = "2025-08-14T16:27:34.653Z" }, + { url = "https://files.pythonhosted.org/packages/11/f3/0bc63a23ac0f8175e23d82f85d6ee693fbd849de7ad739f0a3622182ad29/fonttools-4.59.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:652159e8214eb4856e8387ebcd6b6bd336ee258cbeb639c8be52005b122b9609", size = 2826546, upload-time = "2025-08-14T16:27:36.783Z" }, + { url = "https://files.pythonhosted.org/packages/e9/46/a3968205590e068fdf60e926be329a207782576cb584d3b7dcd2d2844957/fonttools-4.59.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:43d177cd0e847ea026fedd9f099dc917da136ed8792d142298a252836390c478", size = 2359771, upload-time = "2025-08-14T16:27:39.678Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ff/d14b4c283879e8cb57862d9624a34fe6522b6fcdd46ccbfc58900958794a/fonttools-4.59.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e54437651e1440ee53a95e6ceb6ee440b67a3d348c76f45f4f48de1a5ecab019", size = 4831575, upload-time = "2025-08-14T16:27:41.885Z" }, + { url = "https://files.pythonhosted.org/packages/9c/04/a277d9a584a49d98ca12d3b2c6663bdf333ae97aaa83bd0cdabf7c5a6c84/fonttools-4.59.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6065fdec8ff44c32a483fd44abe5bcdb40dd5e2571a5034b555348f2b3a52cea", size = 5069962, upload-time = "2025-08-14T16:27:44.284Z" }, + { url = "https://files.pythonhosted.org/packages/16/6f/3d2ae69d96c4cdee6dfe7598ca5519a1514487700ca3d7c49c5a1ad65308/fonttools-4.59.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42052b56d176f8b315fbc09259439c013c0cb2109df72447148aeda677599612", size = 4942926, upload-time = "2025-08-14T16:27:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d3/c17379e0048d03ce26b38e4ab0e9a98280395b00529e093fe2d663ac0658/fonttools-4.59.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bcd52eaa5c4c593ae9f447c1d13e7e4a00ca21d755645efa660b6999425b3c88", size = 4958678, upload-time = "2025-08-14T16:27:48.555Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3f/c5543a1540abdfb4d375e3ebeb84de365ab9b153ec14cb7db05f537dd1e7/fonttools-4.59.1-cp314-cp314t-win32.whl", hash = "sha256:02e4fdf27c550dded10fe038a5981c29f81cb9bc649ff2eaa48e80dab8998f97", size = 2266706, upload-time = "2025-08-14T16:27:50.556Z" }, + { url = "https://files.pythonhosted.org/packages/3e/99/85bff6e674226bc8402f983e365f07e76d990e7220ba72bcc738fef52391/fonttools-4.59.1-cp314-cp314t-win_amd64.whl", hash = "sha256:412a5fd6345872a7c249dac5bcce380393f40c1c316ac07f447bc17d51900922", size = 2329994, upload-time = "2025-08-14T16:27:52.36Z" }, + { url = "https://files.pythonhosted.org/packages/0f/64/9d606e66d498917cd7a2ff24f558010d42d6fd4576d9dd57f0bd98333f5a/fonttools-4.59.1-py3-none-any.whl", hash = "sha256:647db657073672a8330608970a984d51573557f328030566521bc03415535042", size = 1130094, upload-time = "2025-08-14T16:28:12.048Z" }, ] [[package]] @@ -1952,6 +2101,23 @@ version = "1.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload-time = "2025-06-09T23:00:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload-time = "2025-06-09T23:00:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload-time = "2025-06-09T23:00:44.793Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload-time = "2025-06-09T23:00:46.125Z" }, + { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload-time = "2025-06-09T23:00:47.73Z" }, + { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload-time = "2025-06-09T23:00:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload-time = "2025-06-09T23:00:51.352Z" }, + { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload-time = "2025-06-09T23:00:52.855Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload-time = "2025-06-09T23:00:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload-time = "2025-06-09T23:00:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload-time = "2025-06-09T23:00:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload-time = "2025-06-09T23:01:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload-time = "2025-06-09T23:01:01.474Z" }, + { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload-time = "2025-06-09T23:01:02.961Z" }, + { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload-time = "2025-06-09T23:01:05.095Z" }, + { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload-time = "2025-06-09T23:01:06.54Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload-time = "2025-06-09T23:01:07.752Z" }, { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" }, { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" }, { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" }, @@ -2015,21 +2181,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821, upload-time = "2024-10-26T00:50:33.425Z" }, ] -[[package]] -name = "genson" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c5/cf/2303c8ad276dcf5ee2ad6cf69c4338fd86ef0f471a5207b069adf7a393cf/genson-1.3.0.tar.gz", hash = "sha256:e02db9ac2e3fd29e65b5286f7135762e2cd8a986537c075b06fc5f1517308e37", size = 34919, upload-time = "2024-05-15T22:08:49.123Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/5c/e226de133afd8bb267ec27eead9ae3d784b95b39a287ed404caab39a5f50/genson-1.3.0-py3-none-any.whl", hash = "sha256:468feccd00274cc7e4c09e84b08704270ba8d95232aa280f65b986139cec67f7", size = 21470, upload-time = "2024-05-15T22:08:47.056Z" }, -] - [[package]] name = "gguf" version = "0.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "pyyaml" }, { name = "tqdm" }, @@ -2106,12 +2263,59 @@ wheels = [ ] [[package]] -name = "gprof2dot" -version = "2025.4.14" +name = "gradio" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "anyio" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "brotli" }, + { name = "fastapi" }, + { name = "ffmpy" }, + { name = "gradio-client" }, + { name = "groovy" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, + { name = "orjson" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pydub" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "safehttpx" }, + { name = "semantic-version" }, + { name = "starlette" }, + { name = "tomlkit" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/cb/ce9c99e4026c7daefef2fe6736207a9189571ddefc1277438103e3e413f2/gradio-6.1.0.tar.gz", hash = "sha256:fe9f6757d53ce7840b487a6921151d8c3410f7de6e2152a4407c5eded9ce023a", size = 37852914, upload-time = "2025-12-09T19:31:53.996Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/00/592f02d2f8a815fc3370f3cda70fb2116d6ec31cf3fe33c87fd34d0a1778/gradio-6.1.0-py3-none-any.whl", hash = "sha256:528f17d75c8206da77a4646955678df8a786145b7bdfcba61d14b2fb3cb94b98", size = 22967810, upload-time = "2025-12-09T19:31:51.335Z" }, +] + +[[package]] +name = "gradio-client" +version = "2.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/fd/cad13fa1f7a463a607176432c4affa33ea162f02f58cc36de1d40d3e6b48/gprof2dot-2025.4.14.tar.gz", hash = "sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce", size = 39536, upload-time = "2025-04-14T07:21:45.76Z" } +dependencies = [ + { name = "fsspec" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/cc/b0f04b1c9bf79c7ae9840b9945f5fbd93355719684f83032837695ab1eaf/gradio_client-2.0.1.tar.gz", hash = "sha256:087eb50652370747c0ce66cd0ae79ecb49f9682188d5348e279d44602cbc2814", size = 54792, upload-time = "2025-12-02T01:57:58.685Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/ed/89d760cb25279109b89eb52975a7b5479700d3114a2421ce735bfb2e7513/gprof2dot-2025.4.14-py3-none-any.whl", hash = "sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e", size = 37555, upload-time = "2025-04-14T07:21:43.319Z" }, + { url = "https://files.pythonhosted.org/packages/1b/11/d680ecf4073bd1cacfe9dea57fa95660e4ea2d1fff3125dbaaa902cc9095/gradio_client-2.0.1-py3-none-any.whl", hash = "sha256:6322eecb5963a07703306c0b048bb98518063d05ca99a65fe384417188af8c63", size = 55439, upload-time = "2025-12-02T01:57:57.551Z" }, ] [[package]] @@ -2156,6 +2360,17 @@ version = "3.2.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, + { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, + { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, + { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, + { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" }, + { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, @@ -2178,6 +2393,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, ] +[[package]] +name = "groovy" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/36/bbdede67400277bef33d3ec0e6a31750da972c469f75966b4930c753218f/groovy-0.1.2.tar.gz", hash = "sha256:25c1dc09b3f9d7e292458aa762c6beb96ea037071bf5e917fc81fb78d2231083", size = 17325, upload-time = "2025-02-28T20:24:56.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/27/3d6dcadc8a3214d8522c1e7f6a19554e33659be44546d44a2f7572ac7d2a/groovy-0.1.2-py3-none-any.whl", hash = "sha256:7f7975bab18c729a257a8b1ae9dcd70b7cafb1720481beae47719af57c35fa64", size = 14090, upload-time = "2025-02-28T20:24:55.152Z" }, +] + [[package]] name = "grpcio" version = "1.78.0" @@ -2187,6 +2411,16 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, @@ -2235,39 +2469,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/6d/4d095d27ccd049865ecdafc467754e9e47ad0f677a30dda969c3590f6582/grpcio_reflection-1.78.0-py3-none-any.whl", hash = "sha256:06fcfde9e6888cdd12e9dd1cf6dc7c440c2e9acf420f696ccbe008672ed05b60", size = 22800, upload-time = "2026-02-06T10:01:33.822Z" }, ] -[[package]] -name = "grpcio-tools" -version = "1.78.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "grpcio" }, - { name = "protobuf" }, - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/d1/cbefe328653f746fd319c4377836a25ba64226e41c6a1d7d5cdbc87a459f/grpcio_tools-1.78.0.tar.gz", hash = "sha256:4b0dd86560274316e155d925158276f8564508193088bc43e20d3f5dff956b2b", size = 5393026, upload-time = "2026-02-06T09:59:59.53Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/ce/17311fb77530420e2f441e916b347515133e83d21cd6cc77be04ce093d5b/grpcio_tools-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:2d6de1cc23bdc1baafc23e201b1e48c617b8c1418b4d8e34cebf72141676e5fb", size = 2546284, upload-time = "2026-02-06T09:58:43.073Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d3/79e101483115f0e78223397daef71751b75eba7e92a32060c10aae11ca64/grpcio_tools-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2afeaad88040894c76656202ff832cb151bceb05c0e6907e539d129188b1e456", size = 5705653, upload-time = "2026-02-06T09:58:45.533Z" }, - { url = "https://files.pythonhosted.org/packages/8b/a7/52fa3ccb39ceeee6adc010056eadfbca8198651c113e418dafebbdf2b306/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33cc593735c93c03d63efe7a8ba25f3c66f16c52f0651910712490244facad72", size = 2592788, upload-time = "2026-02-06T09:58:48.918Z" }, - { url = "https://files.pythonhosted.org/packages/68/08/682ff6bb548225513d73dc9403742d8975439d7469c673bc534b9bbc83a7/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2921d7989c4d83b71f03130ab415fa4d66e6693b8b8a1fcbb7a1c67cff19b812", size = 2905157, upload-time = "2026-02-06T09:58:51.478Z" }, - { url = "https://files.pythonhosted.org/packages/b2/66/264f3836a96423b7018e5ada79d62576a6401f6da4e1f4975b18b2be1265/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6a0df438e82c804c7b95e3f311c97c2f876dcc36376488d5b736b7bcf5a9b45", size = 2656166, upload-time = "2026-02-06T09:58:54.117Z" }, - { url = "https://files.pythonhosted.org/packages/f3/6b/f108276611522e03e98386b668cc7e575eff6952f2db9caa15b2a3b3e883/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9c6070a9500798225191ef25d0055a15d2c01c9c8f2ee7b681fffa99c98c822", size = 3109110, upload-time = "2026-02-06T09:58:56.891Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c7/cf048dbcd64b3396b3c860a2ffbcc67a8f8c87e736aaa74c2e505a7eee4c/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:394e8b57d85370a62e5b0a4d64c96fcf7568345c345d8590c821814d227ecf1d", size = 3657863, upload-time = "2026-02-06T09:58:59.176Z" }, - { url = "https://files.pythonhosted.org/packages/b6/37/e2736912c8fda57e2e57a66ea5e0bc8eb9a5fb7ded00e866ad22d50afb08/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3ef700293ab375e111a2909d87434ed0a0b086adf0ce67a8d9cf12ea7765e63", size = 3324748, upload-time = "2026-02-06T09:59:01.242Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/726abc75bb5bfc2841e88ea05896e42f51ca7c30cb56da5c5b63058b3867/grpcio_tools-1.78.0-cp313-cp313-win32.whl", hash = "sha256:6993b960fec43a8d840ee5dc20247ef206c1a19587ea49fe5e6cc3d2a09c1585", size = 993074, upload-time = "2026-02-06T09:59:03.085Z" }, - { url = "https://files.pythonhosted.org/packages/c5/68/91b400bb360faf9b177ffb5540ec1c4d06ca923691ddf0f79e2c9683f4da/grpcio_tools-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:275ce3c2978842a8cf9dd88dce954e836e590cf7029649ad5d1145b779039ed5", size = 1158185, upload-time = "2026-02-06T09:59:05.036Z" }, - { url = "https://files.pythonhosted.org/packages/cf/5e/278f3831c8d56bae02e3acc570465648eccf0a6bbedcb1733789ac966803/grpcio_tools-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:8b080d0d072e6032708a3a91731b808074d7ab02ca8fb9847b6a011fdce64cd9", size = 2546270, upload-time = "2026-02-06T09:59:07.426Z" }, - { url = "https://files.pythonhosted.org/packages/a3/d9/68582f2952b914b60dddc18a2e3f9c6f09af9372b6f6120d6cf3ec7f8b4e/grpcio_tools-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8c0ad8f8f133145cd7008b49cb611a5c6a9d89ab276c28afa17050516e801f79", size = 5705731, upload-time = "2026-02-06T09:59:09.856Z" }, - { url = "https://files.pythonhosted.org/packages/70/68/feb0f9a48818ee1df1e8b644069379a1e6ef5447b9b347c24e96fd258e5d/grpcio_tools-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2f8ea092a7de74c6359335d36f0674d939a3c7e1a550f4c2c9e80e0226de8fe4", size = 2593896, upload-time = "2026-02-06T09:59:12.23Z" }, - { url = "https://files.pythonhosted.org/packages/1f/08/a430d8d06e1b8d33f3e48d3f0cc28236723af2f35e37bd5c8db05df6c3aa/grpcio_tools-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:da422985e0cac822b41822f43429c19ecb27c81ffe3126d0b74e77edec452608", size = 2905298, upload-time = "2026-02-06T09:59:14.458Z" }, - { url = "https://files.pythonhosted.org/packages/71/0a/348c36a3eae101ca0c090c9c3bc96f2179adf59ee0c9262d11cdc7bfe7db/grpcio_tools-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4fab1faa3fbcb246263e68da7a8177d73772283f9db063fb8008517480888d26", size = 2656186, upload-time = "2026-02-06T09:59:16.949Z" }, - { url = "https://files.pythonhosted.org/packages/1d/3f/18219f331536fad4af6207ade04142292faa77b5cb4f4463787988963df8/grpcio_tools-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dd9c094f73f734becae3f20f27d4944d3cd8fb68db7338ee6c58e62fc5c3d99f", size = 3109859, upload-time = "2026-02-06T09:59:19.202Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d9/341ea20a44c8e5a3a18acc820b65014c2e3ea5b4f32a53d14864bcd236bc/grpcio_tools-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2ed51ce6b833068f6c580b73193fc2ec16468e6bc18354bc2f83a58721195a58", size = 3657915, upload-time = "2026-02-06T09:59:21.839Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f4/5978b0f91611a64371424c109dd0027b247e5b39260abad2eaee66b6aa37/grpcio_tools-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:05803a5cdafe77c8bdf36aa660ad7a6a1d9e49bc59ce45c1bade2a4698826599", size = 3324724, upload-time = "2026-02-06T09:59:24.402Z" }, - { url = "https://files.pythonhosted.org/packages/b2/80/96a324dba99cfbd20e291baf0b0ae719dbb62b76178c5ce6c788e7331cb1/grpcio_tools-1.78.0-cp314-cp314-win32.whl", hash = "sha256:f7c722e9ce6f11149ac5bddd5056e70aaccfd8168e74e9d34d8b8b588c3f5c7c", size = 1015505, upload-time = "2026-02-06T09:59:26.3Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d1/909e6a05bfd44d46327dc4b8a78beb2bae4fb245ffab2772e350081aaf7e/grpcio_tools-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d58ade518b546120ec8f0a8e006fc8076ae5df151250ebd7e82e9b5e152c229", size = 1190196, upload-time = "2026-02-06T09:59:28.359Z" }, -] - [[package]] name = "gunicorn" version = "23.0.0" @@ -2374,6 +2575,13 @@ version = "0.6.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683, upload-time = "2024-10-16T19:44:30.175Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337, upload-time = "2024-10-16T19:44:31.786Z" }, + { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796, upload-time = "2024-10-16T19:44:32.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837, upload-time = "2024-10-16T19:44:33.974Z" }, + { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289, upload-time = "2024-10-16T19:44:35.111Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779, upload-time = "2024-10-16T19:44:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634, upload-time = "2024-10-16T19:44:37.357Z" }, { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload-time = "2024-10-16T19:44:38.738Z" }, { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload-time = "2024-10-16T19:44:39.818Z" }, { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload-time = "2024-10-16T19:44:41.189Z" }, @@ -2420,15 +2628,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] -[[package]] -name = "huey" -version = "2.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/29/3428d52eb8e85025e264a291641a9f9d6407cc1e51d1b630f6ac5815999a/huey-2.6.0.tar.gz", hash = "sha256:8d11f8688999d65266af1425b831f6e3773e99415027177b8734b0ffd5e251f6", size = 221068, upload-time = "2026-01-06T03:01:02.055Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/34/fae9ac8f1c3a552fd3f7ff652b94c78d219dedc5fce0c0a4232457760a00/huey-2.6.0-py3-none-any.whl", hash = "sha256:1b9df9d370b49c6d5721ba8a01ac9a787cf86b3bdc584e4679de27b920395c3f", size = 76951, upload-time = "2026-01-06T03:01:00.808Z" }, -] - [[package]] name = "huggingface-hub" version = "1.4.1" @@ -2512,6 +2711,17 @@ version = "3.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f4/57/60d1a6a512f2f0508d0bc8b4f1cc5616fd3196619b66bd6a01f9155a1292/ijson-3.5.0.tar.gz", hash = "sha256:94688760720e3f5212731b3cb8d30267f9a045fb38fb3870254e7b9504246f31", size = 68658, upload-time = "2026-02-24T03:58:30.974Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/17/9c63c7688025f3a8c47ea717b8306649c8c7244e49e20a2be4e3515dc75c/ijson-3.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1ebefbe149a6106cc848a3eaf536af51a9b5ccc9082de801389f152dba6ab755", size = 88536, upload-time = "2026-02-24T03:57:06.809Z" }, + { url = "https://files.pythonhosted.org/packages/6f/dd/e15c2400244c117b06585452ebc63ae254f5a6964f712306afd1422daae0/ijson-3.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:19e30d9f00f82e64de689c0b8651b9cfed879c184b139d7e1ea5030cec401c21", size = 60499, upload-time = "2026-02-24T03:57:09.155Z" }, + { url = "https://files.pythonhosted.org/packages/77/a9/bf4fe3538a0c965f16b406f180a06105b875da83f0743e36246be64ef550/ijson-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a04a33ee78a6f27b9b8528c1ca3c207b1df3b8b867a4cf2fcc4109986f35c227", size = 60330, upload-time = "2026-02-24T03:57:10.574Z" }, + { url = "https://files.pythonhosted.org/packages/31/76/6f91bdb019dd978fce1bc5ea1cd620cfc096d258126c91db2c03a20a7f34/ijson-3.5.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d48dc2984af02eb3c56edfb3f13b3f62f2f3e4fe36f058c8cfc75d93adf4fed", size = 138977, upload-time = "2026-02-24T03:57:11.932Z" }, + { url = "https://files.pythonhosted.org/packages/11/be/bbc983059e48a54b0121ee60042979faed7674490bbe7b2c41560db3f436/ijson-3.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1e73a44844d9adbca9cf2c4132cd875933e83f3d4b23881fcaf82be83644c7d", size = 149785, upload-time = "2026-02-24T03:57:13.255Z" }, + { url = "https://files.pythonhosted.org/packages/6d/81/2fee58f9024a3449aee83edfa7167fb5ccd7e1af2557300e28531bb68e16/ijson-3.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7389a56b8562a19948bdf1d7bae3a2edc8c7f86fb59834dcb1c4c722818e645a", size = 149729, upload-time = "2026-02-24T03:57:14.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/56/f1706761fcc096c9d414b3dcd000b1e6e5c24364c21cfba429837f98ee8d/ijson-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3176f23f8ebec83f374ed0c3b4e5a0c4db7ede54c005864efebbed46da123608", size = 150697, upload-time = "2026-02-24T03:57:15.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6e/ee0d9c875a0193b632b3e9ccd1b22a50685fb510256ad57ba483b6529f77/ijson-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6babd88e508630c6ef86c9bebaaf13bb2fb8ec1d8f8868773a03c20253f599bc", size = 142873, upload-time = "2026-02-24T03:57:16.831Z" }, + { url = "https://files.pythonhosted.org/packages/d2/bf/f9d4399d0e6e3fd615035290a71e97c843f17f329b43638c0a01cf112d73/ijson-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dc1b3836b174b6db2fa8319f1926fb5445abd195dc963368092103f8579cb8ed", size = 151583, upload-time = "2026-02-24T03:57:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/71/a7254a065933c0e2ffd3586f46187d84830d3d7b6f41cfa5901820a4f87d/ijson-3.5.0-cp312-cp312-win32.whl", hash = "sha256:6673de9395fb9893c1c79a43becd8c8fbee0a250be6ea324bfd1487bb5e9ee4c", size = 53079, upload-time = "2026-02-24T03:57:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7b/2edca79b359fc9f95d774616867a03ecccdf333797baf5b3eea79733918c/ijson-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f4f7fabd653459dcb004175235f310435959b1bb5dfa8878578391c6cc9ad944", size = 55500, upload-time = "2026-02-24T03:57:20.428Z" }, { url = "https://files.pythonhosted.org/packages/a2/71/d67e764a712c3590627480643a3b51efcc3afa4ef3cb54ee4c989073c97e/ijson-3.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e9cedc10e40dd6023c351ed8bfc7dcfce58204f15c321c3c1546b9c7b12562a4", size = 88544, upload-time = "2026-02-24T03:57:21.293Z" }, { url = "https://files.pythonhosted.org/packages/1a/39/f1c299371686153fa3cf5c0736b96247a87a1bee1b7145e6d21f359c505a/ijson-3.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3647649f782ee06c97490b43680371186651f3f69bebe64c6083ee7615d185e5", size = 60495, upload-time = "2026-02-24T03:57:22.501Z" }, { url = "https://files.pythonhosted.org/packages/16/94/b1438e204d75e01541bebe3e668fe3e68612d210e9931ae1611062dd0a56/ijson-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90e74be1dce05fce73451c62d1118671f78f47c9f6be3991c82b91063bf01fc9", size = 60325, upload-time = "2026-02-24T03:57:23.332Z" }, @@ -2558,33 +2768,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/28/96711503245339084c8086b892c47415895eba49782d6cc52d9f4ee50301/ijson-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4f24b78d4ef028d17eb57ad1b16c0aed4a17bdd9badbf232dc5d9305b7e13854", size = 58965, upload-time = "2026-02-24T03:58:11.278Z" }, ] -[[package]] -name = "imageio" -version = "2.37.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" } }, - { name = "pillow" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673, upload-time = "2026-03-09T11:31:12.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl", hash = "sha256:46f5bb8522cd421c0f5ae104d8268f569d856b29eb1a13b92829d1970f32c9f0", size = 317646, upload-time = "2026-03-09T11:31:10.771Z" }, -] - -[[package]] -name = "imageio-ffmpeg" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/bd/c3343c721f2a1b0c9fc71c1aebf1966a3b7f08c2eea8ed5437a2865611d6/imageio_ffmpeg-0.6.0.tar.gz", hash = "sha256:e2556bed8e005564a9f925bb7afa4002d82770d6b08825078b7697ab88ba1755", size = 25210, upload-time = "2025-01-16T21:34:32.747Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl", hash = "sha256:9d2baaf867088508d4a3458e61eeb30e945c4ad8016025545f66c4b5aaef0a61", size = 24932969, upload-time = "2025-01-16T21:34:20.464Z" }, - { url = "https://files.pythonhosted.org/packages/40/5c/f3d8a657d362cc93b81aab8feda487317da5b5d31c0e1fdfd5e986e55d17/imageio_ffmpeg-0.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b1ae3173414b5fc5f538a726c4e48ea97edc0d2cdc11f103afee655c463fa742", size = 21113891, upload-time = "2025-01-16T21:34:00.277Z" }, - { url = "https://files.pythonhosted.org/packages/33/e7/1925bfbc563c39c1d2e82501d8372734a5c725e53ac3b31b4c2d081e895b/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1d47bebd83d2c5fc770720d211855f208af8a596c82d17730aa51e815cdee6dc", size = 25632706, upload-time = "2025-01-16T21:33:53.475Z" }, - { url = "https://files.pythonhosted.org/packages/a0/2d/43c8522a2038e9d0e7dbdf3a61195ecc31ca576fb1527a528c877e87d973/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c7e46fcec401dd990405049d2e2f475e2b397779df2519b544b8aab515195282", size = 29498237, upload-time = "2025-01-16T21:34:13.726Z" }, - { url = "https://files.pythonhosted.org/packages/a0/13/59da54728351883c3c1d9fca1710ab8eee82c7beba585df8f25ca925f08f/imageio_ffmpeg-0.6.0-py3-none-win32.whl", hash = "sha256:196faa79366b4a82f95c0f4053191d2013f4714a715780f0ad2a68ff37483cc2", size = 19652251, upload-time = "2025-01-16T21:34:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02fa47c83703c37df6bfe4896aab339013f62bf02c5ebf2dce6da56af04ffc0a", size = 31246824, upload-time = "2025-01-16T21:34:28.6Z" }, -] - [[package]] name = "imagesize" version = "1.4.1" @@ -2696,6 +2879,18 @@ version = "0.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b5/348b3313c58f5fbfb2194eb4d07e46a35748ba6e5b3b3046143f3040bafa/jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b", size = 312262, upload-time = "2025-05-18T19:03:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/9c/4a/6a2397096162b21645162825f058d1709a02965606e537e3304b02742e9b/jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744", size = 320124, upload-time = "2025-05-18T19:03:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/2a/85/1ce02cade7516b726dd88f59a4ee46914bf79d1676d1228ef2002ed2f1c9/jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2", size = 345330, upload-time = "2025-05-18T19:03:47.596Z" }, + { url = "https://files.pythonhosted.org/packages/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670, upload-time = "2025-05-18T19:03:49.334Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057, upload-time = "2025-05-18T19:03:50.66Z" }, + { url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372, upload-time = "2025-05-18T19:03:51.98Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038, upload-time = "2025-05-18T19:03:53.703Z" }, + { url = "https://files.pythonhosted.org/packages/67/27/c62568e3ccb03368dbcc44a1ef3a423cb86778a4389e995125d3d1aaa0a4/jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95", size = 391538, upload-time = "2025-05-18T19:03:55.046Z" }, + { url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557, upload-time = "2025-05-18T19:03:56.386Z" }, + { url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202, upload-time = "2025-05-18T19:03:57.675Z" }, + { url = "https://files.pythonhosted.org/packages/1b/84/5a5d5400e9d4d54b8004c9673bbe4403928a00d28529ff35b19e9d176b19/jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01", size = 211781, upload-time = "2025-05-18T19:03:59.025Z" }, + { url = "https://files.pythonhosted.org/packages/9b/52/7ec47455e26f2d6e5f2ea4951a0652c06e5b995c291f723973ae9e724a65/jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49", size = 206176, upload-time = "2025-05-18T19:04:00.305Z" }, { url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617, upload-time = "2025-05-18T19:04:02.078Z" }, { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947, upload-time = "2025-05-18T19:04:03.347Z" }, { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618, upload-time = "2025-05-18T19:04:04.709Z" }, @@ -2744,45 +2939,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" }, ] -[[package]] -name = "jsonpatch" -version = "1.33" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonpointer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, -] - -[[package]] -name = "jsonpath-ng" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/32/58/250751940d75c8019659e15482d548a4aa3b6ce122c515102a4bfdac50e3/jsonpath_ng-1.8.0.tar.gz", hash = "sha256:54252968134b5e549ea5b872f1df1168bd7defe1a52fed5a358c194e1943ddc3", size = 74513, upload-time = "2026-02-24T14:42:06.182Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/99/33c7d78a3fb70d545fd5411ac67a651c81602cc09c9cf0df383733f068c5/jsonpath_ng-1.8.0-py3-none-any.whl", hash = "sha256:b8dde192f8af58d646fc031fac9c99fe4d00326afc4148f1f043c601a8cfe138", size = 67844, upload-time = "2026-02-28T00:53:19.637Z" }, -] - -[[package]] -name = "jsonpickle" -version = "4.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/a6/d07afcfdef402900229bcca795f80506b207af13a838d4d99ad45abf530c/jsonpickle-4.1.1.tar.gz", hash = "sha256:f86e18f13e2b96c1c1eede0b7b90095bbb61d99fedc14813c44dc2f361dbbae1", size = 316885, upload-time = "2025-06-02T20:36:11.57Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/73/04df8a6fa66d43a9fd45c30f283cc4afff17da671886e451d52af60bdc7e/jsonpickle-4.1.1-py3-none-any.whl", hash = "sha256:bb141da6057898aa2438ff268362b126826c812a1721e31cf08a6e142910dc91", size = 47125, upload-time = "2025-06-02T20:36:08.647Z" }, -] - -[[package]] -name = "jsonpointer" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, -] - [[package]] name = "jsonschema" version = "4.25.1" @@ -2816,6 +2972,12 @@ version = "1.22.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3a/2c/84076b352107ce12d56f28c313f1aca1be332d953dd96aec7b84976e6d53/kaldi-native-fbank-1.22.3.tar.gz", hash = "sha256:387bf87225c6b83c93ae652eeaef1b4d531994b6e398e7a77189de340674f9af", size = 71013, upload-time = "2025-10-09T02:31:21.487Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/de/fbdbfcc75fad9d9a6f9a250bc986f1002902581eaa47a5948f53a7f11851/kaldi_native_fbank-1.22.3-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7f636ccdea28bd187f93b06a1e4b9275e42e43af9405b0684fc739e829299c4b", size = 249003, upload-time = "2025-10-09T02:29:48.509Z" }, + { url = "https://files.pythonhosted.org/packages/77/64/e57ce185dda028b7b9af72cdfb16825bfa52183653945681e7cb8e7c2dfa/kaldi_native_fbank-1.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:abd31a8bfe1db62a7ddb0beee84f3a5de9bb559fcdd2b96ca0fb729c551b9412", size = 228933, upload-time = "2025-10-09T02:31:35.8Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/6f4fd8953c0b3f30de4526fd024095032abcdc25b6736c77a891687c604e/kaldi_native_fbank-1.22.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5a44b4a83cf9bf13d3f77858928068b06d3ec2238c27ff2e39393fbf7749c9f", size = 298887, upload-time = "2025-10-09T02:30:53.739Z" }, + { url = "https://files.pythonhosted.org/packages/84/90/01ef7331c52b1eaf9916f3f7a535155aac2e9e2ddad12a141613d92758c7/kaldi_native_fbank-1.22.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f16e74372fe9e20abb4183f98a8e2288d5ee4c48d04d94b6160311170e007661", size = 322002, upload-time = "2025-10-09T02:30:13.04Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/fce142bd3aeadb1292360a90ceb91f923c8e12081c21576fe69917243c5f/kaldi_native_fbank-1.22.3-cp312-cp312-win32.whl", hash = "sha256:a90f51377569575fc0d1a66ef7e89a36102bfb6dcd1d15d6c4afb930ce726672", size = 273308, upload-time = "2025-10-09T02:29:59.931Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8d/c0b0b6280edabad85d7e15093fad612c027e175fe4e0b960ce2f36485143/kaldi_native_fbank-1.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:cbbeea19fe6d584c54e93fe6615a7185b10e0d78fdb6471f9e44596018437c38", size = 308023, upload-time = "2025-10-09T02:28:43.909Z" }, { url = "https://files.pythonhosted.org/packages/0d/df/4110f685067946c8b2e59ed76cebdf51c979ae999d90f65208a9d1966cba/kaldi_native_fbank-1.22.3-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:78ca163686a4aa1693194d098aa79b517845d851aa6fd27d5b162c05e1012361", size = 249056, upload-time = "2025-10-09T02:28:02.202Z" }, { url = "https://files.pythonhosted.org/packages/d8/74/ef21aabdd2f32539735e2ed4d3ea072112d4e3d30dfc2d17695f6d9df072/kaldi_native_fbank-1.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3768ea99933aa25080cb820f93f7b612968633b9a4fa23bc8a7337e2137f3fbb", size = 229011, upload-time = "2025-10-09T02:31:50.593Z" }, { url = "https://files.pythonhosted.org/packages/9a/72/adb11d27c545aca1db442da744ee430a6aae377a33574bfd2ec159dcf673/kaldi_native_fbank-1.22.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f74b85948328ab4b4c88522f98a59f83dd5295443b08483e945c7de2c35e5dcc", size = 299276, upload-time = "2025-10-09T02:30:38.1Z" }, @@ -2835,6 +2997,19 @@ version = "1.4.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, @@ -2889,86 +3064,23 @@ wheels = [ ] [[package]] -name = "langchain" -version = "0.3.28" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "langchain-core" }, - { name = "langchain-text-splitters" }, - { name = "langsmith" }, - { name = "pydantic" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "sqlalchemy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/87/bb/a65e29c8e4aaf0348c2617962e427c8e760d82a67adbd197019e49c7769d/langchain-0.3.28.tar.gz", hash = "sha256:30a32f44cc6690bcc6a6fb7c14d61a15406d5eda1a0e7eab60b3660944888741", size = 10242473, upload-time = "2026-03-06T22:45:17.911Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/f5/ecd71e5b78e67944b2600a155ef63000bc00148e6794e8e7809b2453887a/langchain-0.3.28-py3-none-any.whl", hash = "sha256:1ba1244477b67b812b775f346209fa596e78bf055a34e45ce22acb7a45842a32", size = 1024717, upload-time = "2026-03-06T22:45:15.545Z" }, -] - -[[package]] -name = "langchain-core" -version = "0.3.84" +name = "kubernetes" +version = "35.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jsonpatch" }, - { name = "langsmith" }, - { name = "packaging" }, - { name = "pydantic" }, + { name = "certifi" }, + { name = "durationpy" }, + { name = "python-dateutil" }, { name = "pyyaml" }, - { name = "tenacity" }, - { name = "typing-extensions" }, - { name = "uuid-utils" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/13/3e/1e70598fac522eaeeeb22f03107da06495160533b25ba4388be9cef01d55/langchain_core-0.3.84.tar.gz", hash = "sha256:814b75bfe67a8460a53f5839bae9505bbfffc7af6f1aa0a5155715563f5cc490", size = 599092, upload-time = "2026-04-08T19:14:00.106Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/5b/ba75d5b80bd1f60ae799c8cbda5477eb7489fb21d40c967ec509bbd51933/langchain_core-0.3.84-py3-none-any.whl", hash = "sha256:d0b3a7b6473e30a2b3d4588ee09dc6471b8d38c46cd48f3e7c3d1ab6547f63cb", size = 459123, upload-time = "2026-04-08T19:13:57.818Z" }, -] - -[[package]] -name = "langchain-nvidia-ai-endpoints" -version = "0.3.19" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "filetype" }, - { name = "langchain-core" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/0fcad6732f124c0d96fd7e8487a0b51db9f167964cfe041e4669a2148dbe/langchain_nvidia_ai_endpoints-0.3.19.tar.gz", hash = "sha256:1c420c88f7c78b2b2c2fdcef8e46104c2dd19c81e036bdd03a4838a6340950fe", size = 42884, upload-time = "2025-10-31T00:17:19.352Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/46/d7529004de384b2abc9e5b76cf4a84a23f3028ec6381bd5f7c00ac39bfab/langchain_nvidia_ai_endpoints-0.3.19-py3-none-any.whl", hash = "sha256:40161a71646fcbe457ac5f2222c5eadcbe31a7d79d618f5a0857c37fffa3a6d5", size = 46229, upload-time = "2025-10-31T00:17:18.306Z" }, -] - -[[package]] -name = "langchain-text-splitters" -version = "0.3.11" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "langchain-core" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/11/43/dcda8fd25f0b19cb2835f2f6bb67f26ad58634f04ac2d8eae00526b0fa55/langchain_text_splitters-0.3.11.tar.gz", hash = "sha256:7a50a04ada9a133bbabb80731df7f6ddac51bc9f1b9cab7fa09304d71d38a6cc", size = 46458, upload-time = "2025-08-31T23:02:58.316Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/0d/41a51b40d24ff0384ec4f7ab8dd3dcea8353c05c973836b5e289f1465d4f/langchain_text_splitters-0.3.11-py3-none-any.whl", hash = "sha256:cf079131166a487f1372c8ab5d0bfaa6c0a4291733d9c43a34a16ac9bcd6a393", size = 33845, upload-time = "2025-08-31T23:02:57.195Z" }, -] - -[[package]] -name = "langsmith" -version = "0.7.26" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "orjson", marker = "platform_python_implementation != 'PyPy' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "packaging" }, - { name = "pydantic" }, { name = "requests" }, - { name = "requests-toolbelt" }, - { name = "uuid-utils" }, - { name = "xxhash" }, - { name = "zstandard" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/86/6de4f6f0451a9658f26f633e0bb090552a4dafd7df3f1ae7f0d40558e67e/langsmith-0.7.26.tar.gz", hash = "sha256:a3e06f3d689ce7195717aa6b8f91082319819ec7ea9b9a62cdcd3d9dc25bfc7b", size = 1146118, upload-time = "2026-04-06T15:01:03.336Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/8f/85bf51ad4150f64e8c665daf0d9dfe9787ae92005efb9a4d1cba592bd79d/kubernetes-35.0.0.tar.gz", hash = "sha256:3d00d344944239821458b9efd484d6df9f011da367ecb155dadf9513f05f09ee", size = 1094642, upload-time = "2026-01-16T01:05:27.76Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/8e/7eb7d65ce62e98e74b9f18f193ea7ac3996d4fbd71fffcc67d0f7ba3103e/langsmith-0.7.26-py3-none-any.whl", hash = "sha256:fe5c877972cea450c1c48251c8fae0f18543c8d19dfdb9ff9a9c4263763dde4e", size = 360160, upload-time = "2026-04-06T15:01:01.516Z" }, + { url = "https://files.pythonhosted.org/packages/0c/70/05b685ea2dffcb2adbf3cdcea5d8865b7bc66f67249084cf845012a0ff13/kubernetes-35.0.0-py2.py3-none-any.whl", hash = "sha256:39e2b33b46e5834ef6c3985ebfe2047ab39135d41de51ce7641a7ca5b372a13d", size = 2017602, upload-time = "2026-01-16T01:05:25.991Z" }, ] [[package]] @@ -2999,6 +3111,17 @@ version = "0.7.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b7/29/47f29026ca17f35cf299290292d5f8331f5077364974b7675a353179afa2/librt-0.7.7.tar.gz", hash = "sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c", size = 145910, upload-time = "2026-01-01T23:52:22.87Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/56/72/1cd9d752070011641e8aee046c851912d5f196ecd726fffa7aed2070f3e0/librt-0.7.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a85a1fc4ed11ea0eb0a632459ce004a2d14afc085a50ae3463cd3dfe1ce43fc", size = 55687, upload-time = "2026-01-01T23:51:16.291Z" }, + { url = "https://files.pythonhosted.org/packages/50/aa/d5a1d4221c4fe7e76ae1459d24d6037783cb83c7645164c07d7daf1576ec/librt-0.7.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c87654e29a35938baead1c4559858f346f4a2a7588574a14d784f300ffba0efd", size = 57136, upload-time = "2026-01-01T23:51:17.363Z" }, + { url = "https://files.pythonhosted.org/packages/23/6f/0c86b5cb5e7ef63208c8cc22534df10ecc5278efc0d47fb8815577f3ca2f/librt-0.7.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c9faaebb1c6212c20afd8043cd6ed9de0a47d77f91a6b5b48f4e46ed470703fe", size = 165320, upload-time = "2026-01-01T23:51:18.455Z" }, + { url = "https://files.pythonhosted.org/packages/16/37/df4652690c29f645ffe405b58285a4109e9fe855c5bb56e817e3e75840b3/librt-0.7.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1908c3e5a5ef86b23391448b47759298f87f997c3bd153a770828f58c2bb4630", size = 174216, upload-time = "2026-01-01T23:51:19.599Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d6/d3afe071910a43133ec9c0f3e4ce99ee6df0d4e44e4bddf4b9e1c6ed41cc/librt-0.7.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbc4900e95a98fc0729523be9d93a8fedebb026f32ed9ffc08acd82e3e181503", size = 189005, upload-time = "2026-01-01T23:51:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/74060a870fe2d9fd9f47824eba6717ce7ce03124a0d1e85498e0e7efc1b2/librt-0.7.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7ea4e1fbd253e5c68ea0fe63d08577f9d288a73f17d82f652ebc61fa48d878d", size = 183961, upload-time = "2026-01-01T23:51:22.493Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5e/918a86c66304af66a3c1d46d54df1b2d0b8894babc42a14fb6f25511497f/librt-0.7.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef7699b7a5a244b1119f85c5bbc13f152cd38240cbb2baa19b769433bae98e50", size = 177610, upload-time = "2026-01-01T23:51:23.874Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d7/b5e58dc2d570f162e99201b8c0151acf40a03a39c32ab824dd4febf12736/librt-0.7.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:955c62571de0b181d9e9e0a0303c8bc90d47670a5eff54cf71bf5da61d1899cf", size = 199272, upload-time = "2026-01-01T23:51:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/87/8202c9bd0968bdddc188ec3811985f47f58ed161b3749299f2c0dd0f63fb/librt-0.7.7-cp312-cp312-win32.whl", hash = "sha256:1bcd79be209313b270b0e1a51c67ae1af28adad0e0c7e84c3ad4b5cb57aaa75b", size = 43189, upload-time = "2026-01-01T23:51:26.799Z" }, + { url = "https://files.pythonhosted.org/packages/61/8d/80244b267b585e7aa79ffdac19f66c4861effc3a24598e77909ecdd0850e/librt-0.7.7-cp312-cp312-win_amd64.whl", hash = "sha256:4353ee891a1834567e0302d4bd5e60f531912179578c36f3d0430f8c5e16b456", size = 49462, upload-time = "2026-01-01T23:51:27.813Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/75db802d6a4992d95e8a889682601af9b49d5a13bbfa246d414eede1b56c/librt-0.7.7-cp312-cp312-win_arm64.whl", hash = "sha256:a76f1d679beccccdf8c1958e732a1dfcd6e749f8821ee59d7bec009ac308c029", size = 42828, upload-time = "2026-01-01T23:51:28.804Z" }, { url = "https://files.pythonhosted.org/packages/8d/5e/d979ccb0a81407ec47c14ea68fb217ff4315521730033e1dd9faa4f3e2c1/librt-0.7.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244", size = 55746, upload-time = "2026-01-01T23:51:29.828Z" }, { url = "https://files.pythonhosted.org/packages/f5/2c/3b65861fb32f802c3783d6ac66fc5589564d07452a47a8cf9980d531cad3/librt-0.7.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d", size = 57174, upload-time = "2026-01-01T23:51:31.226Z" }, { url = "https://files.pythonhosted.org/packages/50/df/030b50614b29e443607220097ebaf438531ea218c7a9a3e21ea862a919cd/librt-0.7.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811", size = 165834, upload-time = "2026-01-01T23:51:32.278Z" }, @@ -3055,6 +3178,11 @@ version = "0.44.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880, upload-time = "2025-01-20T11:14:41.342Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297, upload-time = "2025-01-20T11:13:32.57Z" }, + { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105, upload-time = "2025-01-20T11:13:38.744Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901, upload-time = "2025-01-20T11:13:46.711Z" }, + { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247, upload-time = "2025-01-20T11:13:56.159Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380, upload-time = "2025-01-20T11:14:02.442Z" }, { url = "https://files.pythonhosted.org/packages/89/24/4c0ca705a717514c2092b18476e7a12c74d34d875e05e4d742618ebbf449/llvmlite-0.44.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:319bddd44e5f71ae2689859b7203080716448a3cd1128fb144fe5c055219d516", size = 28132306, upload-time = "2025-01-20T11:14:09.035Z" }, { url = "https://files.pythonhosted.org/packages/01/cf/1dd5a60ba6aee7122ab9243fd614abcf22f36b0437cbbe1ccf1e3391461c/llvmlite-0.44.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c58867118bad04a0bb22a2e0068c693719658105e40009ffe95c7000fcde88e", size = 26201090, upload-time = "2025-01-20T11:14:15.401Z" }, { url = "https://files.pythonhosted.org/packages/d2/1b/656f5a357de7135a3777bd735cc7c9b8f23b4d37465505bd0eaf4be9befe/llvmlite-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46224058b13c96af1365290bdfebe9a6264ae62fb79b2b55693deed11657a8bf", size = 42361904, upload-time = "2025-01-20T11:14:22.949Z" }, @@ -3077,26 +3205,6 @@ 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 = "logsage" -version = "0.1.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "drain3" }, - { name = "langchain" }, - { name = "langchain-core" }, - { name = "langchain-nvidia-ai-endpoints" }, - { name = "nh3" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, - { name = "pandas" }, - { name = "pydantic-settings" }, - { name = "requests" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/eb/d44608c5fc69ac216a41cd164453df5784114d07f28ece6c6ab8a351f636/logsage-0.1.5-py3-none-any.whl", hash = "sha256:4c144ea2b339f370e943929dc2d1f755b04351a75a2d8fd4201bd6d90045ed7a", size = 65098, upload-time = "2026-01-25T10:12:30.042Z" }, -] - [[package]] name = "loguru" version = "0.7.3" @@ -3116,6 +3224,22 @@ version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/89/c3/d01d735c298d7e0ddcedf6f028bf556577e5ab4f4da45175ecd909c79378/lxml-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78718d8454a6e928470d511bf8ac93f469283a45c354995f7d19e77292f26108", size = 8429515, upload-time = "2025-06-26T16:26:06.776Z" }, + { url = "https://files.pythonhosted.org/packages/06/37/0e3eae3043d366b73da55a86274a590bae76dc45aa004b7042e6f97803b1/lxml-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:84ef591495ffd3f9dcabffd6391db7bb70d7230b5c35ef5148354a134f56f2be", size = 4601387, upload-time = "2025-06-26T16:26:09.511Z" }, + { url = "https://files.pythonhosted.org/packages/a3/28/e1a9a881e6d6e29dda13d633885d13acb0058f65e95da67841c8dd02b4a8/lxml-6.0.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2930aa001a3776c3e2601cb8e0a15d21b8270528d89cc308be4843ade546b9ab", size = 5228928, upload-time = "2025-06-26T16:26:12.337Z" }, + { url = "https://files.pythonhosted.org/packages/9a/55/2cb24ea48aa30c99f805921c1c7860c1f45c0e811e44ee4e6a155668de06/lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563", size = 4952289, upload-time = "2025-06-28T18:47:25.602Z" }, + { url = "https://files.pythonhosted.org/packages/31/c0/b25d9528df296b9a3306ba21ff982fc5b698c45ab78b94d18c2d6ae71fd9/lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7", size = 5111310, upload-time = "2025-06-28T18:47:28.136Z" }, + { url = "https://files.pythonhosted.org/packages/e9/af/681a8b3e4f668bea6e6514cbcb297beb6de2b641e70f09d3d78655f4f44c/lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7", size = 5025457, upload-time = "2025-06-26T16:26:15.068Z" }, + { url = "https://files.pythonhosted.org/packages/99/b6/3a7971aa05b7be7dfebc7ab57262ec527775c2c3c5b2f43675cac0458cad/lxml-6.0.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d6e200909a119626744dd81bae409fc44134389e03fbf1d68ed2a55a2fb10991", size = 5657016, upload-time = "2025-07-03T19:19:06.008Z" }, + { url = "https://files.pythonhosted.org/packages/69/f8/693b1a10a891197143c0673fcce5b75fc69132afa81a36e4568c12c8faba/lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da", size = 5257565, upload-time = "2025-06-26T16:26:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/96/e08ff98f2c6426c98c8964513c5dab8d6eb81dadcd0af6f0c538ada78d33/lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e", size = 4713390, upload-time = "2025-06-26T16:26:20.292Z" }, + { url = "https://files.pythonhosted.org/packages/a8/83/6184aba6cc94d7413959f6f8f54807dc318fdcd4985c347fe3ea6937f772/lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741", size = 5066103, upload-time = "2025-06-26T16:26:22.765Z" }, + { url = "https://files.pythonhosted.org/packages/ee/01/8bf1f4035852d0ff2e36a4d9aacdbcc57e93a6cd35a54e05fa984cdf73ab/lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3", size = 4791428, upload-time = "2025-06-26T16:26:26.461Z" }, + { url = "https://files.pythonhosted.org/packages/29/31/c0267d03b16954a85ed6b065116b621d37f559553d9339c7dcc4943a76f1/lxml-6.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c907516d49f77f6cd8ead1322198bdfd902003c3c330c77a1c5f3cc32a0e4d16", size = 5678523, upload-time = "2025-07-03T19:19:09.837Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f7/5495829a864bc5f8b0798d2b52a807c89966523140f3d6fa3a58ab6720ea/lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0", size = 5281290, upload-time = "2025-06-26T16:26:29.406Z" }, + { url = "https://files.pythonhosted.org/packages/79/56/6b8edb79d9ed294ccc4e881f4db1023af56ba451909b9ce79f2a2cd7c532/lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a", size = 3613495, upload-time = "2025-06-26T16:26:31.588Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1e/cc32034b40ad6af80b6fd9b66301fc0f180f300002e5c3eb5a6110a93317/lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3", size = 4014711, upload-time = "2025-06-26T16:26:33.723Z" }, + { url = "https://files.pythonhosted.org/packages/55/10/dc8e5290ae4c94bdc1a4c55865be7e1f31dfd857a88b21cbba68b5fea61b/lxml-6.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:8cb26f51c82d77483cdcd2b4a53cda55bbee29b3c2f3ddeb47182a2a9064e4eb", size = 3674431, upload-time = "2025-06-26T16:26:35.959Z" }, { url = "https://files.pythonhosted.org/packages/79/21/6e7c060822a3c954ff085e5e1b94b4a25757c06529eac91e550f3f5cd8b8/lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da", size = 8414372, upload-time = "2025-06-26T16:26:39.079Z" }, { url = "https://files.pythonhosted.org/packages/a4/f6/051b1607a459db670fc3a244fa4f06f101a8adf86cda263d1a56b3a4f9d5/lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7", size = 4593940, upload-time = "2025-06-26T16:26:41.891Z" }, { url = "https://files.pythonhosted.org/packages/8e/74/dd595d92a40bda3c687d70d4487b2c7eff93fd63b568acd64fedd2ba00fe/lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3", size = 5214329, upload-time = "2025-06-26T16:26:44.669Z" }, @@ -3185,6 +3309,16 @@ version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, @@ -3228,8 +3362,8 @@ dependencies = [ { name = "cycler" }, { name = "fonttools" }, { name = "kiwisolver" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "packaging" }, { name = "pillow" }, { name = "pyparsing" }, @@ -3237,6 +3371,13 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/43/91/f2939bb60b7ebf12478b030e0d7f340247390f402b3b189616aad790c366/matplotlib-3.10.5.tar.gz", hash = "sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076", size = 34804044, upload-time = "2025-07-31T18:09:33.805Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/66/1e/c6f6bcd882d589410b475ca1fc22e34e34c82adff519caf18f3e6dd9d682/matplotlib-3.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:00b6feadc28a08bd3c65b2894f56cf3c94fc8f7adcbc6ab4516ae1e8ed8f62e2", size = 8253056, upload-time = "2025-07-31T18:08:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/53/e6/d6f7d1b59413f233793dda14419776f5f443bcccb2dfc84b09f09fe05dbe/matplotlib-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee98a5c5344dc7f48dc261b6ba5d9900c008fc12beb3fa6ebda81273602cc389", size = 8110131, upload-time = "2025-07-31T18:08:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/66/2b/bed8a45e74957549197a2ac2e1259671cd80b55ed9e1fe2b5c94d88a9202/matplotlib-3.10.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a17e57e33de901d221a07af32c08870ed4528db0b6059dce7d7e65c1122d4bea", size = 8669603, upload-time = "2025-07-31T18:08:09.064Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a7/315e9435b10d057f5e52dfc603cd353167ae28bb1a4e033d41540c0067a4/matplotlib-3.10.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97b9d6443419085950ee4a5b1ee08c363e5c43d7176e55513479e53669e88468", size = 9508127, upload-time = "2025-07-31T18:08:10.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d9/edcbb1f02ca99165365d2768d517898c22c6040187e2ae2ce7294437c413/matplotlib-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ceefe5d40807d29a66ae916c6a3915d60ef9f028ce1927b84e727be91d884369", size = 9566926, upload-time = "2025-07-31T18:08:13.186Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d9/6dd924ad5616c97b7308e6320cf392c466237a82a2040381163b7500510a/matplotlib-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:c04cba0f93d40e45b3c187c6c52c17f24535b27d545f757a2fffebc06c12b98b", size = 8107599, upload-time = "2025-07-31T18:08:15.116Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f3/522dc319a50f7b0279fbe74f86f7a3506ce414bc23172098e8d2bdf21894/matplotlib-3.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:a41bcb6e2c8e79dc99c5511ae6f7787d2fb52efd3d805fff06d5d4f667db16b2", size = 7978173, upload-time = "2025-07-31T18:08:21.518Z" }, { url = "https://files.pythonhosted.org/packages/8d/05/4f3c1f396075f108515e45cb8d334aff011a922350e502a7472e24c52d77/matplotlib-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:354204db3f7d5caaa10e5de74549ef6a05a4550fdd1c8f831ab9bca81efd39ed", size = 8253586, upload-time = "2025-07-31T18:08:23.107Z" }, { url = "https://files.pythonhosted.org/packages/2f/2c/e084415775aac7016c3719fe7006cdb462582c6c99ac142f27303c56e243/matplotlib-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b072aac0c3ad563a2b3318124756cb6112157017f7431626600ecbe890df57a1", size = 8110715, upload-time = "2025-07-31T18:08:24.675Z" }, { url = "https://files.pythonhosted.org/packages/52/1b/233e3094b749df16e3e6cd5a44849fd33852e692ad009cf7de00cf58ddf6/matplotlib-3.10.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d52fd5b684d541b5a51fb276b2b97b010c75bee9aa392f96b4a07aeb491e33c7", size = 8669397, upload-time = "2025-07-31T18:08:26.778Z" }, @@ -3290,14 +3431,14 @@ dependencies = [ { name = "jsonschema" }, { name = "pydantic" }, { name = "pydantic-settings" }, - { name = "pyjwt", extra = ["crypto"] }, + { name = "pyjwt", extra = ["crypto"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "sse-starlette" }, { name = "starlette" }, { name = "typing-extensions" }, { name = "typing-inspection" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } wheels = [ @@ -3332,20 +3473,14 @@ source = { editable = "3rdparty/Megatron-Bridge-workspace" } dependencies = [ { name = "accelerate" }, { name = "causal-conv1d" }, - { name = "comet-ml" }, { name = "datasets" }, - { name = "diffusers" }, - { name = "einops" }, { name = "flash-linear-attention" }, { name = "hydra-core" }, - { name = "imageio" }, - { name = "imageio-ffmpeg" }, { name = "mamba-ssm" }, { name = "mlflow" }, { name = "nvidia-resiliency-ext" }, { name = "omegaconf" }, { name = "open-clip-torch" }, - { name = "peft" }, { name = "pyyaml" }, { name = "qwen-vl-utils" }, { name = "regex" }, @@ -3356,7 +3491,7 @@ dependencies = [ { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "tqdm" }, - { name = "transformer-engine", marker = "extra == 'extra-7-nemo-rl-mcore' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "transformer-engine", extra = ["pytorch"], marker = "extra == 'extra-7-nemo-rl-mcore' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "transformers" }, { name = "typing-extensions" }, { name = "wandb" }, @@ -3366,21 +3501,14 @@ dependencies = [ requires-dist = [ { name = "accelerate" }, { name = "causal-conv1d" }, - { name = "comet-ml", specifier = ">=3.50.0" }, { name = "datasets", specifier = ">=2.20.0" }, - { name = "diffusers", specifier = ">=0.36.0" }, - { name = "einops" }, { name = "flash-linear-attention" }, { name = "hydra-core", specifier = ">1.3,<=1.3.2" }, - { name = "imageio" }, - { name = "imageio-ffmpeg" }, { name = "mamba-ssm" }, - { name = "mlflow", specifier = ">=3.9.0" }, - { name = "nvidia-resiliency-ext" }, + { name = "mlflow", specifier = ">=3.5.0" }, + { name = "nvidia-resiliency-ext", specifier = "~=0.4.1" }, { name = "omegaconf", specifier = ">=2.3.0" }, { name = "open-clip-torch", specifier = ">=3.2.0" }, - { name = "peft", specifier = ">=0.18.0" }, - { name = "peft", specifier = ">=0.18.1" }, { name = "pyyaml", specifier = ">=6.0.2" }, { name = "qwen-vl-utils" }, { name = "regex", specifier = ">=2024.11.6" }, @@ -3391,7 +3519,7 @@ requires-dist = [ { name = "torch", specifier = ">=2.6.0" }, { name = "tqdm", specifier = ">=4.67.1" }, { name = "transformer-engine", extras = ["pytorch", "core-cu12"] }, - { name = "transformers", specifier = ">=5.0.0,<=5.3.0" }, + { name = "transformers", specifier = ">=5.0.0,<=5.2.0" }, { name = "typing-extensions" }, { name = "wandb", specifier = ">=0.25.0" }, ] @@ -3412,8 +3540,9 @@ dependencies = [ { name = "mamba-ssm" }, { name = "megatron-energon", extra = ["av-decode"] }, { name = "multi-storage-client" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, + { name = "nv-grouped-gemm" }, { name = "nvidia-modelopt" }, { name = "nvidia-resiliency-ext" }, { name = "nvtx" }, @@ -3427,7 +3556,7 @@ dependencies = [ { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "sys_platform != 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "tqdm" }, - { name = "transformer-engine" }, + { name = "transformer-engine", extra = ["pytorch"] }, { name = "wget" }, ] @@ -3437,7 +3566,7 @@ requires-dist = [ { name = "causal-conv1d", git = "https://github.com/Dao-AILab/causal-conv1d?rev=67e0a9dfe1518fc0036444e9ab5fe06ab78299e0" }, { name = "datasets" }, { name = "einops", specifier = "~=0.8" }, - { name = "emerging-optimizers", git = "https://github.com/NVIDIA-NeMo/Emerging-Optimizers.git?rev=v0.2.0" }, + { name = "emerging-optimizers", git = "https://github.com/NVIDIA-NeMo/Emerging-Optimizers.git?rev=v0.1.0" }, { name = "fastapi", specifier = "~=0.50" }, { name = "flash-linear-attention", specifier = "~=0.4.0" }, { name = "flashinfer-python", specifier = "~=0.5.0" }, @@ -3446,8 +3575,9 @@ requires-dist = [ { name = "megatron-energon", extras = ["av-decode"], specifier = "~=6.0" }, { name = "multi-storage-client", specifier = "~=0.27" }, { name = "numpy" }, + { name = "nv-grouped-gemm", git = "https://github.com/fanshiqing/grouped_gemm?tag=v1.1.4.post7" }, { name = "nvidia-modelopt", extras = ["torch"], marker = "sys_platform != 'darwin'" }, - { name = "nvidia-resiliency-ext", git = "https://github.com/NVIDIA/nvidia-resiliency-ext.git?rev=15a851565a4ce846c04431ecb0cf09903ab4837e" }, + { name = "nvidia-resiliency-ext" }, { name = "nvtx", specifier = "~=0.2" }, { name = "onnxscript" }, { name = "openai", extras = ["aiohttp"] }, @@ -3459,7 +3589,7 @@ requires-dist = [ { name = "torch", marker = "sys_platform != 'darwin'", specifier = ">=2.6.0", index = "https://download.pytorch.org/whl/cu129" }, { name = "torch", marker = "sys_platform == 'darwin'", specifier = ">=2.6.0", index = "https://pypi.org/simple" }, { name = "tqdm" }, - { name = "transformer-engine", extras = ["core-cu12", "pytorch"] }, + { name = "transformer-engine", extras = ["core-cu12", "pytorch"], specifier = ">=2.9.0a0,<2.12.0" }, { name = "wget" }, ] @@ -3471,8 +3601,8 @@ dependencies = [ { name = "braceexpand" }, { name = "click" }, { name = "multi-storage-client" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "pillow" }, { name = "pyyaml" }, { name = "s3fs" }, @@ -3517,11 +3647,11 @@ version = "1.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonschema" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-sglang' or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "pillow" }, { name = "pydantic" }, - { name = "pydantic-extra-types", extra = ["pycountry"], marker = "extra == 'extra-7-nemo-rl-automodel' or extra != 'extra-7-nemo-rl-mcore' or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "pydantic-extra-types", extra = ["pycountry"], marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-vllm' or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang')" }, { name = "requests" }, { name = "tiktoken" }, { name = "typing-extensions" }, @@ -3550,35 +3680,17 @@ sentencepiece = [ name = "ml-dtypes" version = "0.5.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 's390x' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (platform_machine != 's390x' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 's390x' and extra == 'extra-7-nemo-rl-automodel') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-mcore') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/a7/aad060393123cfb383956dca68402aff3db1e1caffd5764887ed5153f41b/ml_dtypes-0.5.3.tar.gz", hash = "sha256:95ce33057ba4d05df50b1f3cfefab22e351868a843b3b15a46c65836283670c9", size = 692316, upload-time = "2025-07-29T18:39:19.454Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/eb/bc07c88a6ab002b4635e44585d80fa0b350603f11a2097c9d1bfacc03357/ml_dtypes-0.5.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:156418abeeda48ea4797db6776db3c5bdab9ac7be197c1233771e0880c304057", size = 663864, upload-time = "2025-07-29T18:38:33.777Z" }, + { url = "https://files.pythonhosted.org/packages/cf/89/11af9b0f21b99e6386b6581ab40fb38d03225f9de5f55cf52097047e2826/ml_dtypes-0.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1db60c154989af253f6c4a34e8a540c2c9dce4d770784d426945e09908fbb177", size = 4951313, upload-time = "2025-07-29T18:38:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a9/b98b86426c24900b0c754aad006dce2863df7ce0bb2bcc2c02f9cc7e8489/ml_dtypes-0.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b255acada256d1fa8c35ed07b5f6d18bc21d1556f842fbc2d5718aea2cd9e55", size = 4928805, upload-time = "2025-07-29T18:38:38.29Z" }, + { url = "https://files.pythonhosted.org/packages/50/c1/85e6be4fc09c6175f36fb05a45917837f30af9a5146a5151cb3a3f0f9e09/ml_dtypes-0.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:da65e5fd3eea434ccb8984c3624bc234ddcc0d9f4c81864af611aaebcc08a50e", size = 208182, upload-time = "2025-07-29T18:38:39.72Z" }, + { url = "https://files.pythonhosted.org/packages/9e/17/cf5326d6867be057f232d0610de1458f70a8ce7b6290e4b4a277ea62b4cd/ml_dtypes-0.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:8bb9cd1ce63096567f5f42851f5843b5a0ea11511e50039a7649619abfb4ba6d", size = 161560, upload-time = "2025-07-29T18:38:41.072Z" }, { url = "https://files.pythonhosted.org/packages/2d/87/1bcc98a66de7b2455dfb292f271452cac9edc4e870796e0d87033524d790/ml_dtypes-0.5.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5103856a225465371fe119f2fef737402b705b810bd95ad5f348e6e1a6ae21af", size = 663781, upload-time = "2025-07-29T18:38:42.984Z" }, { url = "https://files.pythonhosted.org/packages/fd/2c/bd2a79ba7c759ee192b5601b675b180a3fd6ccf48ffa27fe1782d280f1a7/ml_dtypes-0.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cae435a68861660af81fa3c5af16b70ca11a17275c5b662d9c6f58294e0f113", size = 4956217, upload-time = "2025-07-29T18:38:44.65Z" }, { url = "https://files.pythonhosted.org/packages/14/f3/091ba84e5395d7fe5b30c081a44dec881cd84b408db1763ee50768b2ab63/ml_dtypes-0.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6936283b56d74fbec431ca57ce58a90a908fdbd14d4e2d22eea6d72bb208a7b7", size = 4933109, upload-time = "2025-07-29T18:38:46.405Z" }, @@ -3597,49 +3709,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3c/541c4b30815ab90ebfbb51df15d0b4254f2f9f1e2b4907ab229300d5e6f2/ml_dtypes-0.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ab039ffb40f3dc0aeeeba84fd6c3452781b5e15bef72e2d10bcb33e4bbffc39", size = 5285284, upload-time = "2025-07-29T18:39:11.532Z" }, ] -[[package]] -name = "ml-dtypes" -version = "0.5.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "platform_machine == 's390x' and sys_platform == 'linux'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux'", - "platform_machine == 's390x' and sys_platform == 'darwin'", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 's390x' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (platform_machine == 's390x' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 's390x' and extra == 'extra-7-nemo-rl-automodel') or (platform_machine == 's390x' and extra == 'extra-7-nemo-rl-mcore') or (platform_machine == 's390x' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, -] -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/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" }, - { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" }, - { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" }, - { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" }, - { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" }, - { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" }, - { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" }, - { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" }, - { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" }, - { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" }, - { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" }, - { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" }, - { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" }, - { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" }, - { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" }, - { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" }, - { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" }, - { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" }, - { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" }, -] - [[package]] name = "mlflow" -version = "3.11.1" +version = "3.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiohttp" }, { name = "alembic" }, { name = "cryptography" }, { name = "docker" }, @@ -3647,28 +3721,26 @@ dependencies = [ { name = "flask-cors" }, { name = "graphene" }, { name = "gunicorn", marker = "sys_platform != 'win32' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "huey" }, { name = "matplotlib" }, { name = "mlflow-skinny" }, { name = "mlflow-tracing" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "pandas" }, { name = "pyarrow" }, { name = "scikit-learn" }, { name = "scipy" }, - { name = "skops" }, { name = "sqlalchemy" }, { name = "waitress", marker = "sys_platform == 'win32' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/34/e328c073cd32c186fb242a957e5bade82433c06bc45b7d1695bf4d02f166/mlflow-3.11.1.tar.gz", hash = "sha256:84e54c4be91b5b2a19039a2673fe688b1d7307ceddacc08af51f8df05b19ee56", size = 9797469, upload-time = "2026-04-07T14:26:58.463Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/7e/516ba65bfa6f5857904ce18bcb738234004663dae1197cee082d48f1ad29/mlflow-3.5.1.tar.gz", hash = "sha256:32630f2aaadeb6dc6ccbde56247a1500518b38d0a7cc12f714be1703b6ee3ea1", size = 8300179, upload-time = "2025-10-22T18:11:47.263Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/62/96826c340354638dfedcbdbcd35d67754566bd45f6592300e0c215c80e30/mlflow-3.11.1-py3-none-any.whl", hash = "sha256:8f6bf1238ac04f97664c229dd480380c5c254a78bdb3c0e433e3a0397508b1af", size = 10479141, upload-time = "2026-04-07T14:26:55.709Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/33cf2596dfbdfe49c2a4696e4321a90e835faeb46e590980461d1d4ef811/mlflow-3.5.1-py3-none-any.whl", hash = "sha256:ebbf5fef59787161a15f2878f210877a62d54d943ad6cea140621687b2393f85", size = 8773271, upload-time = "2025-10-22T18:11:44.6Z" }, ] [[package]] name = "mlflow-skinny" -version = "3.11.1" +version = "3.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -3691,14 +3763,14 @@ dependencies = [ { name = "typing-extensions" }, { name = "uvicorn" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/77/fe2027ddad9e52ed1ac360fbc262169e6366f6678632e350cbd0d901bb9b/mlflow_skinny-3.11.1.tar.gz", hash = "sha256:86ce63491349f6713afc8a4ef0bf77a8314d0e79e03753cb150d6c860a0b0475", size = 2642799, upload-time = "2026-04-07T14:26:43.818Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/1a/ede3fb7a4085bf640e2842c0a4d3d95ef665b21e6d0e92cfb7867ba58ef7/mlflow_skinny-3.5.1.tar.gz", hash = "sha256:4358a5489221cdecf53cf045e10df28919dedb9489965434ce3445f7cbabf365", size = 1927869, upload-time = "2025-10-22T17:58:41.623Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/a7/e61ec397b34dc3c9e91572f45e41617f429d5c524d38a4e1aa2316ee1b5e/mlflow_skinny-3.11.1-py3-none-any.whl", hash = "sha256:82ffd5f6980320b4ac19f741e7a754faa1d01707e632b002ea68e04fd25a0535", size = 3171551, upload-time = "2026-04-07T14:26:41.762Z" }, + { url = "https://files.pythonhosted.org/packages/b4/88/75690e7cdc6fe56374e24178055bb2a7385e1e29c51a8cbb2fb747892af1/mlflow_skinny-3.5.1-py3-none-any.whl", hash = "sha256:e5f96977d21a093a3ffda789bee90070855dbfe1b9d0703c0c3e34d2f8d7fba8", size = 2314304, upload-time = "2025-10-22T17:58:39.526Z" }, ] [[package]] name = "mlflow-tracing" -version = "3.11.1" +version = "3.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -3710,9 +3782,55 @@ dependencies = [ { name = "protobuf" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/77/73af163432f3c66e2d213045250972e504a6683c76f63dd1abfba441a16a/mlflow_tracing-3.11.1.tar.gz", hash = "sha256:cb63cee16385d081467ec5bee4807fe1af59ddfdf04be4c79e7a7813b1002193", size = 1314550, upload-time = "2026-04-07T14:26:32.785Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/38/ade11b09edfee133078015656aec8a3854f1a6ed1bd6e6d9af333fcdaaf9/mlflow_tracing-3.5.1.tar.gz", hash = "sha256:bca266b1871692ae2ec812ed177cdc108ccef1cb3fb82725a8b959ec98d5fba0", size = 1056089, upload-time = "2025-10-22T17:56:12.047Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/7f/99006f6c261ef694363e8599ad858c223aa9918231e8bd7a1569041967ac/mlflow_tracing-3.5.1-py3-none-any.whl", hash = "sha256:4fd685347158e0d2c48f5bec3d15ecfc6fadc1dbb48073cb220ded438408fa65", size = 1273904, upload-time = "2025-10-22T17:56:10.748Z" }, +] + +[[package]] +name = "mlx" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mlx-metal", marker = "sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/c9/d12ed6a8393450e28eb1f552b50200f83f138b1268b5f4e8074a76d745a2/mlx-0.28.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:97866d5e454e8f2d7bc42aadcbfd7565d40f4755564785e4fb964812fbad604b", size = 564160, upload-time = "2025-08-07T07:50:34.652Z" }, + { url = "https://files.pythonhosted.org/packages/71/4f/3951766a5edb75c0d2d860381f592d271b4c3b7241e730e78dd63926f5b4/mlx-0.28.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5204ebf399439e5da374295f6c1b6961355824604eed7026c18edfe4c83e9243", size = 540098, upload-time = "2025-08-07T07:50:52.67Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/cb8eb03544eace055a500bd4a3b776a3ce48198d7b7b398e21a5a3256e89/mlx-0.28.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:34776bd3fe97bca7c6c76d77f6104e0d6b05b3626bb3cf9ed48d3a9bbd46c180", size = 540100, upload-time = "2025-08-07T07:50:49.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/cb2703724f011d9aabc5a5a8ddf20481fdbdf251227468de89885b71f0ed/mlx-0.28.0-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:ead9a6c13b704239b5ca50d68f5effc505c8f15a6017f35d8b4d0e25832e29dd", size = 632943, upload-time = "2025-08-07T07:52:35.695Z" }, + { url = "https://files.pythonhosted.org/packages/cd/fb/795f3540057642bcf3a95fe7d17c14ffaca2102511328eee6cd92d49223e/mlx-0.28.0-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:78c88e5cc4188f538935b23803e10eaf084caa8bfeaa2a6de983038ecee3fd78", size = 564139, upload-time = "2025-08-07T07:50:31.487Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4a/39609e5e3fea14c429e8a61f9754e61e4ed5289422223ad213df9116fd55/mlx-0.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:0b7a57a584ea5e807ec0a17c4eb179a71e01eeff9f25dff6950abad1e30443c2", size = 540205, upload-time = "2025-08-07T07:50:47.284Z" }, + { url = "https://files.pythonhosted.org/packages/43/af/738ea855df6742a4ac4ee1c72f298ff6cf50f0af7e553e89a1a41060c12c/mlx-0.28.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:a7cdcbd3faff45c18e9f51f95e9aa9410c71bbb4d5d86878a97eb996a0467505", size = 540201, upload-time = "2025-08-07T07:50:45.122Z" }, + { url = "https://files.pythonhosted.org/packages/ae/23/3cb550734765e136147ac8e05851cc9b64c2397518148367ed1db346d09c/mlx-0.28.0-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:91f76bea8a192b423fa2b458a4c293c6b36e2b4b7f13a15eea94f0dc6ae03f13", size = 633209, upload-time = "2025-08-07T07:53:32.143Z" }, +] + +[[package]] +name = "mlx-lm" +version = "0.26.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2", marker = "sys_platform == 'darwin'" }, + { name = "mlx", marker = "sys_platform == 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "protobuf", marker = "sys_platform == 'darwin'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin'" }, + { name = "transformers", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/4b/ed8ec01f182203b0897415a9d20f0cd8a141def77ad43deea18ffaba4c9c/mlx_lm-0.26.3.tar.gz", hash = "sha256:06cd74ee3eea920335c528e68feb854eede45fe4e5f149b464ac100c1dbeaded", size = 172096, upload-time = "2025-08-06T21:48:22.762Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/ab/d980c84e7df4224ab8db2457afbe135b430f371ca081a37cf89f8ef18ca1/mlflow_tracing-3.11.1-py3-none-any.whl", hash = "sha256:fa82df64dacf8293b714ae666440fe7c1902c6470c024df389bb91e9de3106d9", size = 1575790, upload-time = "2026-04-07T14:26:30.804Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ff/142ba3ec53282e179bab3ba5608c5edec3b419bcc816df63c141bcc6e2e9/mlx_lm-0.26.3-py3-none-any.whl", hash = "sha256:c6a9e44bd707822bc165ce638723ab11252e8334b7b3bf79c7d399c8c3d6d48e", size = 235187, upload-time = "2025-08-06T21:48:21.73Z" }, +] + +[[package]] +name = "mlx-metal" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/71/879284c71dfb12ded986a6532a4ab7df5c2794385ccf2766c1b40aee74cb/mlx_metal-0.28.0-py3-none-macosx_13_0_arm64.whl", hash = "sha256:ce08d40f1fad4f0b3bc87bfff5d603c7fe7dd141c082ba9ce9328b41e8f8d46b", size = 33840007, upload-time = "2025-08-07T07:53:07.437Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/44a261ccb9f6052c93c9da4faa4fc6d4f914938c51ecbbb68c546ab521b9/mlx_metal-0.28.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:424142ab843e2ac0b14edb58cf88d96723823c565291f46ddeeaa072abcc991e", size = 33196759, upload-time = "2025-08-07T07:52:59.436Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/8e4dee2893a56fc68a27eec7ec7ed9559c7ea01099313a9b8196373bf3cf/mlx_metal-0.28.0-py3-none-macosx_15_0_arm64.whl", hash = "sha256:214ece3781d44f57eb9686561594b28915ec5568df4a5a73da59c66880b204ed", size = 33167706, upload-time = "2025-08-07T07:53:03.852Z" }, ] [[package]] @@ -3764,6 +3882,16 @@ version = "1.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/45/b1/ea4f68038a18c77c9467400d166d74c4ffa536f34761f7983a104357e614/msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd", size = 173555, upload-time = "2025-06-13T06:52:51.324Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/389b9c593eda2b8551b2e7126ad3a06af6f9b44274eb3a4f054d48ff7e47/msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238", size = 82359, upload-time = "2025-06-13T06:52:03.909Z" }, + { url = "https://files.pythonhosted.org/packages/ab/65/7d1de38c8a22cf8b1551469159d4b6cf49be2126adc2482de50976084d78/msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157", size = 79172, upload-time = "2025-06-13T06:52:05.246Z" }, + { url = "https://files.pythonhosted.org/packages/0f/bd/cacf208b64d9577a62c74b677e1ada005caa9b69a05a599889d6fc2ab20a/msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce", size = 425013, upload-time = "2025-06-13T06:52:06.341Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ec/fd869e2567cc9c01278a736cfd1697941ba0d4b81a43e0aa2e8d71dab208/msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a", size = 426905, upload-time = "2025-06-13T06:52:07.501Z" }, + { url = "https://files.pythonhosted.org/packages/55/2a/35860f33229075bce803a5593d046d8b489d7ba2fc85701e714fc1aaf898/msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c", size = 407336, upload-time = "2025-06-13T06:52:09.047Z" }, + { url = "https://files.pythonhosted.org/packages/8c/16/69ed8f3ada150bf92745fb4921bd621fd2cdf5a42e25eb50bcc57a5328f0/msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b", size = 409485, upload-time = "2025-06-13T06:52:10.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b6/0c398039e4c6d0b2e37c61d7e0e9d13439f91f780686deb8ee64ecf1ae71/msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef", size = 412182, upload-time = "2025-06-13T06:52:11.644Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d0/0cf4a6ecb9bc960d624c93effaeaae75cbf00b3bc4a54f35c8507273cda1/msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a", size = 419883, upload-time = "2025-06-13T06:52:12.806Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/9697c211720fa71a2dfb632cad6196a8af3abea56eece220fde4674dc44b/msgpack-1.1.1-cp312-cp312-win32.whl", hash = "sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c", size = 65406, upload-time = "2025-06-13T06:52:14.271Z" }, + { url = "https://files.pythonhosted.org/packages/c0/23/0abb886e80eab08f5e8c485d6f13924028602829f63b8f5fa25a06636628/msgpack-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4", size = 72558, upload-time = "2025-06-13T06:52:15.252Z" }, { url = "https://files.pythonhosted.org/packages/a1/38/561f01cf3577430b59b340b51329803d3a5bf6a45864a55f4ef308ac11e3/msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0", size = 81677, upload-time = "2025-06-13T06:52:16.64Z" }, { url = "https://files.pythonhosted.org/packages/09/48/54a89579ea36b6ae0ee001cba8c61f776451fad3c9306cd80f5b5c55be87/msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9", size = 78603, upload-time = "2025-06-13T06:52:17.843Z" }, { url = "https://files.pythonhosted.org/packages/a0/60/daba2699b308e95ae792cdc2ef092a38eb5ee422f9d2fbd4101526d8a210/msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8", size = 420504, upload-time = "2025-06-13T06:52:18.982Z" }, @@ -3782,6 +3910,13 @@ version = "0.19.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934, upload-time = "2024-12-27T17:40:28.597Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485, upload-time = "2024-12-27T17:39:44.974Z" }, + { url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910, upload-time = "2024-12-27T17:39:46.401Z" }, + { url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633, upload-time = "2024-12-27T17:39:49.099Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc", size = 213594, upload-time = "2024-12-27T17:39:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c", size = 214053, upload-time = "2024-12-27T17:39:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537", size = 219081, upload-time = "2024-12-27T17:39:55.142Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0", size = 187467, upload-time = "2024-12-27T17:39:56.531Z" }, { url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86", size = 190498, upload-time = "2024-12-27T17:40:00.427Z" }, { url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314", size = 183950, upload-time = "2024-12-27T17:40:04.219Z" }, { url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e", size = 210647, upload-time = "2024-12-27T17:40:05.606Z" }, @@ -3811,6 +3946,10 @@ dependencies = [ { name = "xattr" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/28/a2/46320db394150a2f0547930b902e8ad045a084fb519f408e2c9b4ca673a0/multi_storage_client-0.36.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2058e8e8f8fd9eef033171b0bf1966596e9862c7f20c2886101ad979996c453b", size = 5293778, upload-time = "2025-11-26T20:07:11.731Z" }, + { url = "https://files.pythonhosted.org/packages/00/2d/658af3b4104c4f2aa2621469482dca8270490601e98d8f7997361499adaa/multi_storage_client-0.36.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:22b69c7f3c9ffa166f38bafa7e08f6b664a5dbee8c88d5d740bed719e6f410a1", size = 5418642, upload-time = "2025-11-26T19:58:15.717Z" }, + { url = "https://files.pythonhosted.org/packages/09/2f/6441794bf8dc195d614d63ad2b7068ad7703972fd6f960d43202d29748b1/multi_storage_client-0.36.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b384fb326637e79706ff706e60f384b24fdbcc824420bb66ef615a9ef5ffb4ec", size = 3194133, upload-time = "2025-11-26T20:05:54.618Z" }, + { url = "https://files.pythonhosted.org/packages/0e/ba/b07361ff84e5bd263e299b03776382f59bd92862573c915dd705a09f3c1d/multi_storage_client-0.36.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7111567b971a68719c0eb68245d49a0a3c3bf5af2f609351446f20ac3e83c0d5", size = 3364563, upload-time = "2025-11-26T20:04:20.3Z" }, { url = "https://files.pythonhosted.org/packages/f9/4a/cbd61589a457e2f4fbacd08b7e7dd11cdb74690857f4b40042844b1ff894/multi_storage_client-0.36.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8137558d5f05e4722c54540e2d6067ea61e9ce3d736fa9cb5c541c7f94d1b48", size = 5293550, upload-time = "2025-11-26T20:03:36.459Z" }, { url = "https://files.pythonhosted.org/packages/a7/3d/7499a9d537fa950a9acf11604b1f9372ed2cadd582b55f1c7cb885ce6f40/multi_storage_client-0.36.0-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:5394c5e040c32433b42e902d9fcf03f8a475c5c9ff1cca80743b2cb944c8af9e", size = 5417538, upload-time = "2025-11-26T20:06:16.782Z" }, { url = "https://files.pythonhosted.org/packages/d7/c3/1b1adc3b3b8569d258a34dbedb6a8c51fc94b947b2df276e251f0f1e23a2/multi_storage_client-0.36.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:195e8c8d57d812b73efd41b96cd60825c484d317ec86379fad3e435e9365a4a6", size = 3193426, upload-time = "2025-11-26T20:00:56.034Z" }, @@ -3823,6 +3962,24 @@ version = "6.6.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/05/f6/512ffd8fd8b37fb2680e5ac35d788f1d71bbaf37789d21a820bdc441e565/multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", size = 76516, upload-time = "2025-08-11T12:06:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/58/45c3e75deb8855c36bd66cc1658007589662ba584dbf423d01df478dd1c5/multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", size = 45394, upload-time = "2025-08-11T12:06:54.555Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/e8c4472a93a26e4507c0b8e1f0762c0d8a32de1328ef72fd704ef9cc5447/multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", size = 43591, upload-time = "2025-08-11T12:06:55.672Z" }, + { url = "https://files.pythonhosted.org/packages/05/51/edf414f4df058574a7265034d04c935aa84a89e79ce90fcf4df211f47b16/multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", size = 237215, upload-time = "2025-08-11T12:06:57.213Z" }, + { url = "https://files.pythonhosted.org/packages/c8/45/8b3d6dbad8cf3252553cc41abea09ad527b33ce47a5e199072620b296902/multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", size = 258299, upload-time = "2025-08-11T12:06:58.946Z" }, + { url = "https://files.pythonhosted.org/packages/3c/e8/8ca2e9a9f5a435fc6db40438a55730a4bf4956b554e487fa1b9ae920f825/multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", size = 242357, upload-time = "2025-08-11T12:07:00.301Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/80c77c99df05a75c28490b2af8f7cba2a12621186e0a8b0865d8e745c104/multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", size = 268369, upload-time = "2025-08-11T12:07:01.638Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e9/920bfa46c27b05fb3e1ad85121fd49f441492dca2449c5bcfe42e4565d8a/multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", size = 269341, upload-time = "2025-08-11T12:07:02.943Z" }, + { url = "https://files.pythonhosted.org/packages/af/65/753a2d8b05daf496f4a9c367fe844e90a1b2cac78e2be2c844200d10cc4c/multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", size = 256100, upload-time = "2025-08-11T12:07:04.564Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/655be13ae324212bf0bc15d665a4e34844f34c206f78801be42f7a0a8aaa/multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", size = 253584, upload-time = "2025-08-11T12:07:05.914Z" }, + { url = "https://files.pythonhosted.org/packages/5c/74/ab2039ecc05264b5cec73eb018ce417af3ebb384ae9c0e9ed42cb33f8151/multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", size = 251018, upload-time = "2025-08-11T12:07:08.301Z" }, + { url = "https://files.pythonhosted.org/packages/af/0a/ccbb244ac848e56c6427f2392741c06302bbfba49c0042f1eb3c5b606497/multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", size = 251477, upload-time = "2025-08-11T12:07:10.248Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b0/0ed49bba775b135937f52fe13922bc64a7eaf0a3ead84a36e8e4e446e096/multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", size = 263575, upload-time = "2025-08-11T12:07:11.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/7fb85a85e14de2e44dfb6a24f03c41e2af8697a6df83daddb0e9b7569f73/multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", size = 259649, upload-time = "2025-08-11T12:07:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/03/9e/b3a459bcf9b6e74fa461a5222a10ff9b544cb1cd52fd482fb1b75ecda2a2/multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", size = 251505, upload-time = "2025-08-11T12:07:14.57Z" }, + { url = "https://files.pythonhosted.org/packages/86/a2/8022f78f041dfe6d71e364001a5cf987c30edfc83c8a5fb7a3f0974cff39/multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", size = 41888, upload-time = "2025-08-11T12:07:15.904Z" }, + { url = "https://files.pythonhosted.org/packages/c7/eb/d88b1780d43a56db2cba24289fa744a9d216c1a8546a0dc3956563fd53ea/multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", size = 46072, upload-time = "2025-08-11T12:07:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/b929320bf5750e2d9d4931835a4c638a19d2494a5b519caaaa7492ebe105/multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", size = 43222, upload-time = "2025-08-11T12:07:18.328Z" }, { url = "https://files.pythonhosted.org/packages/3a/5d/e1db626f64f60008320aab00fbe4f23fc3300d75892a3381275b3d284580/multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", size = 75848, upload-time = "2025-08-11T12:07:19.912Z" }, { url = "https://files.pythonhosted.org/packages/4c/aa/8b6f548d839b6c13887253af4e29c939af22a18591bfb5d0ee6f1931dae8/multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", size = 45060, upload-time = "2025-08-11T12:07:21.163Z" }, { url = "https://files.pythonhosted.org/packages/eb/c6/f5e97e5d99a729bc2aa58eb3ebfa9f1e56a9b517cc38c60537c81834a73f/multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", size = 43269, upload-time = "2025-08-11T12:07:22.392Z" }, @@ -3890,6 +4047,12 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, @@ -3947,11 +4110,13 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cuda-core" }, { name = "cuda-pathfinder" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "packaging" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/92/c25fe7de52c8d04f9508c30278883c8ac28156756a4c695fbdb835133db7/nccl4py-0.1.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb329f9089da84c312691ca2167ff283c4b1c66737b302f9cbb8da925d3619db", size = 1616398, upload-time = "2026-01-09T20:15:13.351Z" }, + { url = "https://files.pythonhosted.org/packages/93/6d/760015411dc833fb4497a09a50def572983ad8c0bfd73655810317eedcaa/nccl4py-0.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b674aa1cde12aa75ac0875112e5232f2dc23ee82863a16102931b308321b94c", size = 1637976, upload-time = "2026-01-09T20:15:14.727Z" }, { url = "https://files.pythonhosted.org/packages/59/f2/6af704fdfcd9fa33f12af5d502bf8e24daa680700183ac4d53178b8962e8/nccl4py-0.1.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:796e09f1d476c858bb78361ba617945bb7506adeedf998945beba8dad897512e", size = 1597209, upload-time = "2026-01-09T20:15:23.855Z" }, { url = "https://files.pythonhosted.org/packages/61/9b/57c558dc9e755eadcefa98a65a57d6303c4d555f4b7afcacbff124ff85ae/nccl4py-0.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c543a32c104488d73c9dd4e5391c3e936369bd2fd1aea8b1f2b68bfee09e537", size = 1621018, upload-time = "2026-01-09T20:15:25.126Z" }, { url = "https://files.pythonhosted.org/packages/77/ac/fabb9f05909b6be57d616a639f60865bc14c420d8966c203d2a313298224/nccl4py-0.1.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca10d214cbdff8901b78bd09f04080939b903b4a7c2f56657c718901cbcee9ad", size = 1588928, upload-time = "2026-01-09T20:15:27.842Z" }, @@ -3985,7 +4150,7 @@ moe = [ { name = "mamba-ssm" }, { name = "nv-grouped-gemm" }, { name = "onnxscript" }, - { name = "transformer-engine", marker = "extra == 'extra-7-nemo-rl-automodel' or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "transformer-engine", extra = ["pytorch"], marker = "extra == 'extra-7-nemo-rl-automodel' or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] [package.metadata] @@ -4041,7 +4206,7 @@ requires-dist = [ { name = "torchvision", marker = "sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'diffusion'", index = "https://download.pytorch.org/whl/cpu" }, { name = "torchvision", marker = "sys_platform == 'linux' and extra == 'diffusion'", index = "https://download.pytorch.org/whl/cu129" }, { name = "transformer-engine", extras = ["pytorch"], marker = "extra == 'cuda'", specifier = "<=2.11.0" }, - { name = "transformers", specifier = ">=5.3.0,<5.4.0" }, + { name = "transformers", specifier = ">=5.3.0" }, { name = "wandb" }, ] provides-extras = ["diffusion", "cuda", "cuda-source", "extra", "fa", "delta-databricks", "moe", "vlm", "all"] @@ -4091,27 +4256,19 @@ dependencies = [ { name = "datasets" }, { name = "devtools" }, { name = "fastapi" }, - { name = "fonttools" }, - { name = "gprof2dot" }, + { name = "gradio" }, { name = "hydra-core" }, - { name = "itsdangerous" }, { name = "mlflow" }, - { name = "mlflow-skinny" }, { name = "omegaconf" }, { name = "openai" }, { name = "orjson" }, { name = "psutil" }, { name = "pydantic" }, { name = "pydantic-core" }, - { name = "pydot" }, - { name = "python-multipart" }, { name = "ray", extra = ["default"] }, - { name = "rich" }, { name = "tqdm" }, - { name = "urllib3" }, { name = "uvicorn" }, { name = "uvloop" }, - { name = "wandb" }, { name = "yappi" }, ] @@ -4144,40 +4301,32 @@ docs = [ [package.metadata] requires-dist = [ - { name = "aiohttp", specifier = ">=3.13.3" }, + { name = "aiohttp" }, { name = "coverage", extras = ["toml"], marker = "extra == 'dev'" }, { name = "datasets" }, { name = "devtools" }, { name = "fastapi" }, - { name = "fonttools", specifier = ">=4.60.2" }, - { name = "gprof2dot" }, + { name = "gradio" }, { name = "hydra-core" }, - { name = "itsdangerous" }, { name = "mlflow" }, - { name = "mlflow-skinny" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8.0" }, { name = "omegaconf" }, - { name = "openai", specifier = "<=2.7.2" }, + { name = "openai", specifier = "<=2.6.1" }, { name = "orjson" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.6.0" }, { name = "psutil" }, { name = "pydantic" }, { name = "pydantic-core" }, - { name = "pydot" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "pytest-asyncio", marker = "extra == 'dev'" }, { name = "pytest-cov", marker = "extra == 'dev'" }, { name = "pytest-xdist", marker = "extra == 'dev'" }, - { name = "python-multipart", specifier = ">=0.0.22" }, { name = "ray", extras = ["default"] }, { name = "requests-mock", marker = "extra == 'dev'" }, - { name = "rich" }, { name = "ruff", marker = "extra == 'dev'" }, { name = "tqdm" }, - { name = "urllib3", specifier = ">=2.6.3" }, { name = "uvicorn" }, { name = "uvloop" }, - { name = "wandb" }, { name = "yappi" }, ] provides-extras = ["dev"] @@ -4215,11 +4364,12 @@ dependencies = [ { name = "nccl4py" }, { name = "ninja" }, { name = "num2words" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "nvidia-cudnn-cu12" }, { name = "nvidia-ml-py" }, { name = "nvidia-nvshmem-cu12", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "nvidia-resiliency-ext" }, { name = "nvtx" }, { name = "omegaconf" }, { name = "pillow" }, @@ -4230,7 +4380,6 @@ dependencies = [ { name = "ray", extra = ["default"] }, { name = "rich" }, { name = "setuptools" }, - { name = "soundfile" }, { name = "swanlab" }, { name = "sympy" }, { name = "tensorboard" }, @@ -4241,7 +4390,7 @@ dependencies = [ { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "torchvision", version = "0.25.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "sys_platform != 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "transformers" }, - { name = "triton", version = "3.6.0", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "triton", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "wandb" }, ] @@ -4253,7 +4402,7 @@ automodel = [ { name = "mamba-ssm" }, { name = "nemo-automodel", extra = ["moe"], marker = "extra == 'extra-7-nemo-rl-automodel' or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "nv-grouped-gemm" }, - { name = "transformer-engine", marker = "extra == 'extra-7-nemo-rl-automodel' or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "transformer-engine", extra = ["pytorch"], marker = "extra == 'extra-7-nemo-rl-automodel' or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "transformers" }, ] fsdp = [ @@ -4261,30 +4410,29 @@ fsdp = [ { name = "flash-attn" }, { name = "mamba-ssm" }, ] +k8s = [ + { name = "kubernetes" }, +] mcore = [ { name = "deep-ep" }, { name = "emerging-optimizers" }, { name = "flash-attn" }, { name = "megatron-bridge" }, { name = "megatron-core" }, - { name = "transformer-engine", marker = "extra == 'extra-7-nemo-rl-mcore' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "transformer-engine", extra = ["pytorch"], marker = "extra == 'extra-7-nemo-rl-mcore' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, ] nemo-gym = [ { name = "nemo-gym" }, ] -nvrx = [ - { name = "nvidia-resiliency-ext" }, -] sglang = [ + { name = "sgl-kernel" }, { name = "sglang" }, - { name = "sglang-kernel" }, ] vllm = [ - { name = "cuda-python", version = "12.9.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "cuda-python", version = "13.0.1", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "cuda-python", version = "12.9.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "cuda-python", version = "13.0.1", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "deep-ep" }, { name = "deep-gemm" }, - { name = "flashinfer-cubin" }, { name = "flashinfer-python" }, { name = "num2words" }, { name = "nvidia-cutlass-dsl" }, @@ -4346,20 +4494,20 @@ requires-dist = [ { name = "deep-ep", marker = "extra == 'mcore'", git = "https://github.com/deepseek-ai/DeepEP.git?rev=bfded34800dfec415b71503f8205181de90b2480" }, { name = "deep-ep", marker = "extra == 'vllm'", git = "https://github.com/deepseek-ai/DeepEP.git?rev=bfded34800dfec415b71503f8205181de90b2480" }, { name = "deep-gemm", marker = "extra == 'vllm'", git = "https://github.com/deepseek-ai/DeepGEMM.git?rev=7b6b5563b9d4c1ae07ffbce7f78ad3ac9204827c" }, - { name = "emerging-optimizers", marker = "extra == 'mcore'", git = "https://github.com/NVIDIA-NeMo/Emerging-Optimizers.git?rev=v0.2.0" }, + { name = "emerging-optimizers", marker = "extra == 'mcore'", specifier = "==0.1.0" }, { name = "flash-attn", marker = "extra == 'automodel'", specifier = "==2.8.1" }, { name = "flash-attn", marker = "extra == 'fsdp'", specifier = "==2.8.1" }, { name = "flash-attn", marker = "extra == 'mcore'", specifier = "==2.8.1" }, - { name = "flashinfer-cubin", marker = "extra == 'vllm'", specifier = "==0.6.4" }, { name = "flashinfer-python", marker = "extra == 'vllm'", specifier = "==0.6.4" }, { name = "hydra-core" }, + { name = "kubernetes", marker = "extra == 'k8s'" }, { name = "mamba-ssm", marker = "extra == 'automodel'", git = "https://github.com/state-spaces/mamba.git?rev=d68d16ed7d5d5164eb5a57c0285f3b7eb8394ec1" }, { name = "mamba-ssm", marker = "extra == 'fsdp'", git = "https://github.com/state-spaces/mamba.git?rev=d68d16ed7d5d5164eb5a57c0285f3b7eb8394ec1" }, { name = "math-verify" }, { name = "matplotlib" }, { name = "megatron-bridge", marker = "extra == 'mcore'", editable = "3rdparty/Megatron-Bridge-workspace" }, { name = "megatron-core", marker = "extra == 'mcore'", editable = "3rdparty/Megatron-LM-workspace" }, - { name = "mlflow", specifier = ">=3.11.1" }, + { name = "mlflow", specifier = ">=3.5.0,<3.6.0" }, { name = "nccl4py" }, { name = "nemo-automodel", extras = ["moe"], marker = "extra == 'automodel'", editable = "3rdparty/Automodel-workspace/Automodel" }, { name = "nemo-gym", marker = "extra == 'nemo-gym'", editable = "3rdparty/Gym-workspace/Gym" }, @@ -4372,10 +4520,10 @@ requires-dist = [ { name = "nvidia-cutlass-dsl", marker = "extra == 'vllm'", specifier = ">=4.4.0.dev1" }, { name = "nvidia-ml-py" }, { name = "nvidia-nvshmem-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nvidia-resiliency-ext", marker = "extra == 'nvrx'" }, + { name = "nvidia-resiliency-ext" }, { name = "nvtx" }, { name = "omegaconf" }, - { name = "pillow", specifier = ">=12.1.1" }, + { name = "pillow", specifier = ">=11.3.0" }, { name = "pip" }, { name = "plotly" }, { name = "pybase64" }, @@ -4383,9 +4531,8 @@ requires-dist = [ { name = "ray", extras = ["default"], specifier = "==2.54.0" }, { name = "rich" }, { name = "setuptools" }, - { name = "sglang", marker = "extra == 'sglang'", git = "https://github.com/sgl-project/sglang.git?subdirectory=python&tag=v0.5.10" }, - { name = "sglang-kernel", marker = "extra == 'sglang'", git = "https://github.com/sgl-project/sglang.git?subdirectory=sgl-kernel&tag=v0.5.10" }, - { name = "soundfile", specifier = ">=0.13.1" }, + { name = "sgl-kernel", marker = "extra == 'sglang'", git = "https://github.com/JustinTong0323/sglang.git?subdirectory=sgl-kernel&rev=70aa688742dd2b75bf9e8e980249303f39295b0d" }, + { name = "sglang", marker = "extra == 'sglang'", git = "https://github.com/JustinTong0323/sglang.git?subdirectory=python&rev=70aa688742dd2b75bf9e8e980249303f39295b0d" }, { name = "swanlab" }, { name = "sympy", specifier = ">=1.14.0" }, { name = "tensorboard" }, @@ -4395,15 +4542,15 @@ requires-dist = [ { name = "torchdata" }, { name = "torchvision", marker = "sys_platform != 'darwin'", specifier = "==0.25.0", index = "https://download.pytorch.org/whl/cu129" }, { name = "torchvision", marker = "sys_platform == 'darwin'", specifier = "==0.25.0", index = "https://pypi.org/simple" }, - { name = "transformer-engine", extras = ["core-cu12", "pytorch"], marker = "extra == 'mcore'", git = "https://github.com/NVIDIA/TransformerEngine.git?rev=71bbefbf153418f943640df0f7373625dc93fa46" }, { name = "transformer-engine", extras = ["pytorch"], marker = "extra == 'automodel'", specifier = ">=2.9.0a0,<2.12.0" }, - { name = "transformers", specifier = "==5.3.0" }, - { name = "transformers", marker = "extra == 'automodel'", specifier = ">=5.3.0" }, + { name = "transformer-engine", extras = ["pytorch"], marker = "extra == 'mcore'", specifier = "==2.12.0" }, + { name = "transformers", specifier = "==5.2.0" }, + { name = "transformers", marker = "extra == 'automodel'", specifier = ">=5.0.0" }, { name = "triton", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')", index = "https://download.pytorch.org/whl/cu129" }, - { name = "vllm", marker = "extra == 'vllm'", specifier = "==0.17.1" }, - { name = "wandb", specifier = ">=0.25.1" }, + { name = "vllm", marker = "extra == 'vllm'", specifier = "==0.17.0" }, + { name = "wandb", specifier = ">=0.25.0" }, ] -provides-extras = ["fsdp", "automodel", "vllm", "sglang", "mcore", "nvrx", "nemo-gym"] +provides-extras = ["fsdp", "automodel", "vllm", "sglang", "mcore", "nemo-gym", "k8s"] [package.metadata.requires-dev] build = [ @@ -4437,13 +4584,22 @@ docs = [ { name = "swagger-plugin-for-sphinx" }, ] test = [ - { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest", specifier = ">=7.0.0" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-testmon" }, { name = "pytest-timeout" }, ] +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + [[package]] name = "networkx" version = "3.5" @@ -4453,40 +4609,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, ] -[[package]] -name = "nh3" -version = "0.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/86/f8d3a7c9bd1bbaa181f6312c757e0b74d25f71ecf84ea3c0dc5e0f01840d/nh3-0.3.4.tar.gz", hash = "sha256:96709a379997c1b28c8974146ca660b0dcd3794f4f6d50c1ea549bab39ac6ade", size = 19520, upload-time = "2026-03-25T10:57:30.789Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/5e/c400663d14be2216bc084ed2befc871b7b12563f85d40904f2a4bf0dd2b7/nh3-0.3.4-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8b61058f34c2105d44d2a4d4241bacf603a1ef5c143b08766bbd0cf23830118f", size = 1417991, upload-time = "2026-03-25T10:56:59.13Z" }, - { url = "https://files.pythonhosted.org/packages/36/f5/109526f5002ec41322ac8cafd50f0f154bae0c26b9607c0fcb708bdca8ec/nh3-0.3.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:554cc2bab281758e94d770c3fb0bf2d8be5fb403ef6b2e8841dd7c1615df7a0f", size = 790566, upload-time = "2026-03-25T10:57:00.445Z" }, - { url = "https://files.pythonhosted.org/packages/7b/66/38950f2b4b316ffd82ee51ed8f9143d1f56fdd620312cacc91613b77b3e7/nh3-0.3.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dbe76feaa44e2ef9436f345016012a591550e77818876a8de5c8bc2a248e08df", size = 837538, upload-time = "2026-03-25T10:57:01.848Z" }, - { url = "https://files.pythonhosted.org/packages/d8/9f/9d6da970e9524fe360ea02a2082856390c2c8ba540409d1be6e5851887b3/nh3-0.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:87dac8d611b4a478400e0821a13b35770e88c266582f065e7249d6a37b0f86e8", size = 1012154, upload-time = "2026-03-25T10:57:03.592Z" }, - { url = "https://files.pythonhosted.org/packages/54/92/7c85c33c241e9dd51dda115bd3f765e940446588cdaaca62ef8edffe675f/nh3-0.3.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8d697e19f2995b337f648204848ac3a528eaafffc39e7ce4ac6b7a2fbe6c84af", size = 1092516, upload-time = "2026-03-25T10:57:04.726Z" }, - { url = "https://files.pythonhosted.org/packages/16/0f/597842bdb2890999a3faa2f3fcb02db8aa6ad09320d3d843ff6d0a1f737b/nh3-0.3.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:7cae217f031809321db962cd7e092bda8d4e95a87f78c0226628fa6c2ea8ebc5", size = 1053793, upload-time = "2026-03-25T10:57:06.171Z" }, - { url = "https://files.pythonhosted.org/packages/7d/32/669da65147bc10746d2e1d7a8a3dbfbffe0315f419e74b559e2ee3471a01/nh3-0.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:07999b998bf89692738f15c0eac76a416382932f855709e0b7488b595c30ec89", size = 1035975, upload-time = "2026-03-25T10:57:07.292Z" }, - { url = "https://files.pythonhosted.org/packages/a1/7e/9e97a8b3c5161c79b4bf21cc54e9334860a52cc54ede15bf2239ef494b73/nh3-0.3.4-cp314-cp314t-win32.whl", hash = "sha256:ca90397c8d36c1535bf1988b2bed006597337843a164c7ec269dc8813f37536b", size = 600419, upload-time = "2026-03-25T10:57:08.342Z" }, - { url = "https://files.pythonhosted.org/packages/e0/c7/6849d8d4295d3997d148eacb2d4b1c9faada4895ee3c1b1e12e72f4611e2/nh3-0.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:41e46b3499918ab6128b6421677b316e79869d0c140da24069d220a94f4e72d1", size = 613342, upload-time = "2026-03-25T10:57:09.593Z" }, - { url = "https://files.pythonhosted.org/packages/8b/0e/14a3f510f36c20b922c123a2730f071f938d006fb513aacfd46d6cbc03a7/nh3-0.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:80b955d802bf365bd42e09f6c3d64567dce777d20e97968d94b3e9d9e99b265e", size = 607025, upload-time = "2026-03-25T10:57:10.959Z" }, - { url = "https://files.pythonhosted.org/packages/4a/57/a97955bc95960cfb1f0517043d60a121f4ba93fde252d4d9ffd3c2a9eead/nh3-0.3.4-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d8bebcb20ab4b91858385cd98fe58046ec4a624275b45ef9b976475604f45b49", size = 1439519, upload-time = "2026-03-25T10:57:12.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/c9a33361da8cde7c7760f091cd10467bc470634e4eea31c8bb70935b00a4/nh3-0.3.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d825722a1e8cbc87d7ca1e47ffb1d2a6cf343ad4c1b8465becf7cadcabcdfd0", size = 833798, upload-time = "2026-03-25T10:57:13.264Z" }, - { url = "https://files.pythonhosted.org/packages/6b/19/9487790780b8c94eacca37866c1270b747a4af8e244d43b3b550fddbbf62/nh3-0.3.4-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4aa8b43e68c26b68069a3b6cef09de166d1d7fa140cf8d77e409a46cbf742e44", size = 820414, upload-time = "2026-03-25T10:57:14.236Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b4/c6a340dd321d20b1e4a663307032741da045685c87403926c43656f6f5ec/nh3-0.3.4-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f5f214618ad5eff4f2a6b13a8d4da4d9e7f37c569d90a13fb9f0caaf7d04fe21", size = 1061531, upload-time = "2026-03-25T10:57:15.384Z" }, - { url = "https://files.pythonhosted.org/packages/c4/49/f6b4b474e0032e4bcbb7174b44e4cf6915670e09c62421deb06ccfcb88b8/nh3-0.3.4-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3390e4333883673a684ce16c1716b481e91782d6f56dec5c85fed9feedb23382", size = 1021889, upload-time = "2026-03-25T10:57:16.454Z" }, - { url = "https://files.pythonhosted.org/packages/43/da/e52a6941746d1f974752af3fc8591f1dbcdcf7fd8c726c7d99f444ba820e/nh3-0.3.4-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a2e44ccb29cbb45071b8f3f2dab9ebfb41a6516f328f91f1f1fd18196239a4", size = 912965, upload-time = "2026-03-25T10:57:17.624Z" }, - { url = "https://files.pythonhosted.org/packages/d6/b7/ec1cbc6b297a808c513f59f501656389623fc09ad6a58c640851289c7854/nh3-0.3.4-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0961a27dc2057c38d0364cb05880e1997ae1c80220cbc847db63213720b8f304", size = 804975, upload-time = "2026-03-25T10:57:18.994Z" }, - { url = "https://files.pythonhosted.org/packages/a9/56/b1275aa2c6510191eed76178da4626b0900402439cb9f27d6b9bf7c6d5e9/nh3-0.3.4-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:9337517edb7c10228252cce2898e20fb3d77e32ffaccbb3c66897927d74215a0", size = 833400, upload-time = "2026-03-25T10:57:20.086Z" }, - { url = "https://files.pythonhosted.org/packages/7c/a5/5d574ffa3c6e49a5364d1b25ebad165501c055340056671493beb467a15e/nh3-0.3.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d866701affe67a5171b916b5c076e767a74c6a9efb7fb2006eb8d3c5f9a293d5", size = 854277, upload-time = "2026-03-25T10:57:21.433Z" }, - { url = "https://files.pythonhosted.org/packages/79/36/8aeb2ab21517cefa212db109e41024e02650716cb42bf293d0a88437a92d/nh3-0.3.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:47d749d99ae005ab19517224140b280dd56e77b33afb82f9b600e106d0458003", size = 1022021, upload-time = "2026-03-25T10:57:22.433Z" }, - { url = "https://files.pythonhosted.org/packages/9c/95/9fd860997685e64abe2d5a995ca2eb5004c0fb6d6585429612a7871548b9/nh3-0.3.4-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f987cb56458323405e8e5ea827e1befcf141ffa0c0ac797d6d02e6b646056d9a", size = 1103526, upload-time = "2026-03-25T10:57:23.487Z" }, - { url = "https://files.pythonhosted.org/packages/7d/0d/df545070614c1007f0109bb004230226c9000e7857c9785583ec25cda9d7/nh3-0.3.4-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:883d5a6d6ee8078c4afc8e96e022fe579c4c265775ff6ee21e39b8c542cabab3", size = 1068050, upload-time = "2026-03-25T10:57:24.624Z" }, - { url = "https://files.pythonhosted.org/packages/94/d5/17b016df52df052f714c53be71df26a1943551d9931e9383b92c998b88f8/nh3-0.3.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:75643c22f5092d8e209f766ee8108c400bc1e44760fc94d2d638eb138d18f853", size = 1046037, upload-time = "2026-03-25T10:57:25.799Z" }, - { url = "https://files.pythonhosted.org/packages/51/39/49f737907e6ab2b4ca71855d3bd63dd7958862e9c8b94fb4e5b18ccf6988/nh3-0.3.4-cp38-abi3-win32.whl", hash = "sha256:72e4e9ca1c4bd41b4a28b0190edc2e21e3f71496acd36a0162858e1a28db3d7e", size = 609542, upload-time = "2026-03-25T10:57:27.112Z" }, - { url = "https://files.pythonhosted.org/packages/73/4f/af8e9071d7464575a7316831938237ffc9d92d27f163dbdd964b1309cd9b/nh3-0.3.4-cp38-abi3-win_amd64.whl", hash = "sha256:c10b1f0c741e257a5cb2978d6bac86e7c784ab20572724b20c6402c2e24bce75", size = 624244, upload-time = "2026-03-25T10:57:28.302Z" }, - { url = "https://files.pythonhosted.org/packages/44/0c/37695d6b0168f6714b5c492331636a9e6123d6ec22d25876c68d06eab1b8/nh3-0.3.4-cp38-abi3-win_arm64.whl", hash = "sha256:43ad4eedee7e049b9069bc015b7b095d320ed6d167ecec111f877de1540656e9", size = 616649, upload-time = "2026-03-25T10:57:29.623Z" }, -] - [[package]] name = "ninja" version = "1.13.0" @@ -4544,6 +4666,11 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615, upload-time = "2025-04-09T02:58:07.659Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/a0/c6b7b9c615cfa3b98c4c63f4316e3f6b3bbe2387740277006551784218cd/numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2", size = 2776626, upload-time = "2025-04-09T02:57:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/92/4a/fe4e3c2ecad72d88f5f8cd04e7f7cff49e718398a2fac02d2947480a00ca/numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8", size = 2779287, upload-time = "2025-04-09T02:57:53.658Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2d/e518df036feab381c23a624dac47f8445ac55686ec7f11083655eb707da3/numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546", size = 3885928, upload-time = "2025-04-09T02:57:55.206Z" }, + { url = "https://files.pythonhosted.org/packages/10/0f/23cced68ead67b75d77cfcca3df4991d1855c897ee0ff3fe25a56ed82108/numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd", size = 3577115, upload-time = "2025-04-09T02:57:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/68/1d/ddb3e704c5a8fb90142bf9dc195c27db02a08a99f037395503bfbc1d14b3/numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18", size = 2831929, upload-time = "2025-04-09T02:57:58.45Z" }, { url = "https://files.pythonhosted.org/packages/0b/f3/0fe4c1b1f2569e8a18ad90c159298d862f96c3964392a20d74fc628aee44/numba-0.61.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:3a10a8fc9afac40b1eac55717cece1b8b1ac0b946f5065c89e00bde646b5b154", size = 2771785, upload-time = "2025-04-09T02:57:59.96Z" }, { url = "https://files.pythonhosted.org/packages/e9/71/91b277d712e46bd5059f8a5866862ed1116091a7cb03bd2704ba8ebe015f/numba-0.61.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d3bcada3c9afba3bed413fba45845f2fb9cd0d2b27dd58a1be90257e293d140", size = 2773289, upload-time = "2025-04-09T02:58:01.435Z" }, { url = "https://files.pythonhosted.org/packages/0d/e0/5ea04e7ad2c39288c0f0f9e8d47638ad70f28e275d092733b5817cf243c9/numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdbca73ad81fa196bd53dc12e3aaf1564ae036e0c125f237c7644fe64a4928ab", size = 3893918, upload-time = "2025-04-09T02:58:02.933Z" }, @@ -4556,16 +4683,27 @@ name = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "platform_machine == 'aarch64' and sys_platform == 'linux'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux'", - "platform_machine == 's390x' and sys_platform == 'linux'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux'", - "platform_machine != 's390x' and sys_platform == 'darwin'", - "platform_machine == 's390x' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version < '3.13' and sys_platform == 'darwin'", ] sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, @@ -4593,34 +4731,58 @@ name = "numpy" version = "2.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", ] sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, @@ -4670,13 +4832,27 @@ name = "nv-grouped-gemm" version = "1.1.4.post7" source = { git = "https://github.com/fanshiqing/grouped_gemm?tag=v1.1.4.post7#6dfaf60e6112166b8b82e9210b51c7f557956f0a" } dependencies = [ - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "setuptools" }, - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-automodel') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "sys_platform != 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "wheel" }, ] +[[package]] +name = "nvdlfw-inspect" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "sys_platform != 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/86/94188e03e5d4dd7b73c390b0cddcde5618b3799c18e327b2bf15763f6137/nvdlfw_inspect-0.2.2-py3-none-any.whl", hash = "sha256:8a4dc2814c5a4cd19ae304170b9bfa514538ef3c3eb243a45a82404ec3cb279d", size = 30964, upload-time = "2025-12-03T10:52:01.933Z" }, +] + [[package]] name = "nvidia-cublas-cu12" version = "12.9.1.4" @@ -4735,6 +4911,9 @@ name = "nvidia-cudnn-frontend" version = "1.16.0" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/00/39/79b606e805abd67ab4fa72f752a5413a496159f10d94fbdb1d67bb5ae86c/nvidia_cudnn_frontend-1.16.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd6fdd71c0896ff2ca1809d914cbd17f2904d55863f8881f47946e1d634c7a88", size = 1839271, upload-time = "2025-11-07T01:29:53.06Z" }, + { url = "https://files.pythonhosted.org/packages/09/21/a0e0d50ba8d7b639fe635500fee0d9c0319561b1ae72176d7024ec04b439/nvidia_cudnn_frontend-1.16.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:16efb069d4bda4d3b99134f59f376cfd4d09558298bd96af778fdc7f2851e696", size = 1954062, upload-time = "2025-11-07T01:32:18.556Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d6/30ae67bb9c010e9459d1211c56d73373eb4e3dd9f57f4c3c1fe0966efcb1/nvidia_cudnn_frontend-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:7b7860db03767c158accbe0b4e9c9553506513cc970ff08ed28c7761681ac466", size = 1368435, upload-time = "2025-11-07T01:26:28.022Z" }, { url = "https://files.pythonhosted.org/packages/32/2c/b4376afef0a6342c56e82e3465c1f8f5c719f588293a50dd04019a22ae6e/nvidia_cudnn_frontend-1.16.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6bcb3a2fbff80538958e21e2227520f082a961164865aaeedaac527f61084f9", size = 1839805, upload-time = "2025-11-07T01:30:31.056Z" }, { url = "https://files.pythonhosted.org/packages/71/13/836b90354036154ab82db3861210e5736983fe1fc44bb39c146ad93b333b/nvidia_cudnn_frontend-1.16.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbdad88b2bec5dde837f8fa7632022334cddb4756f923b5421c06a712cb59d31", size = 1953953, upload-time = "2025-11-07T01:33:03.781Z" }, { url = "https://files.pythonhosted.org/packages/e5/30/3025f34f2c86ceef85134dc1f323f8cf2a26d3ffddc5ada48528c80bfae1/nvidia_cudnn_frontend-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:138de2bc4697fabb2eb2f0f601a7e31f8fe97874908e26e33d737276f335473c", size = 1368359, upload-time = "2025-11-07T01:26:51.561Z" }, @@ -4828,11 +5007,13 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cuda-python", version = "12.9.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin' or extra == 'extra-7-nemo-rl-sglang' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "cuda-python", version = "13.0.1", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-fsdp') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "typing-extensions" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/7d/0df5e38d11e52cc72095a14d6448bc1c5d0d4b00b069a1189ca417fb225b/nvidia_cutlass_dsl_libs_base-4.4.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2ec8812eeadcbb6fe20bda2e295ed9c00653f8253b78e33cf0ab65a47b829e73", size = 75473821, upload-time = "2026-03-16T02:27:08.371Z" }, + { url = "https://files.pythonhosted.org/packages/56/98/e264964741d9cc9816625d9600d17a5249fd5cbd8c2d166fb0d0c34dfe5a/nvidia_cutlass_dsl_libs_base-4.4.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:22e37b58f7a6f2f43bba533c4df8a088012122e0b4e9a632eca23937adeafb39", size = 74355593, upload-time = "2026-03-16T02:25:11.762Z" }, { url = "https://files.pythonhosted.org/packages/1b/c9/2f17950ee2deb4b5f6b82f8155515a21792fe296e81bb638f164d8e2ca9b/nvidia_cutlass_dsl_libs_base-4.4.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b59a052cbfb9a25747d1b6d413615456bea38d1f377da085af07c0d86a4c8b39", size = 75477304, upload-time = "2026-03-16T02:27:35.645Z" }, { url = "https://files.pythonhosted.org/packages/e1/68/27380038ebd9c8eab4be364e833fea144aef597704f44948921668f7adf4/nvidia_cutlass_dsl_libs_base-4.4.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8e3324a33afa7424e93beae7e54a311e80db82b9e4ed4bba2aeeda1d6c888cd9", size = 74355765, upload-time = "2026-03-16T02:24:16.778Z" }, { url = "https://files.pythonhosted.org/packages/12/44/0dc7f2e5b5c65106a5bb05e60654f1a79abe92e27e9b00588a73cd26ca1f/nvidia_cutlass_dsl_libs_base-4.4.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:af96c1170569138b3cb965202907fbf5ab95d7c1dcc210952d00cdf9ab7b859a", size = 75472171, upload-time = "2026-03-16T02:28:03.136Z" }, @@ -4854,8 +5035,8 @@ version = "0.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ninja" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "nvidia-ml-py" }, { name = "packaging" }, { name = "pulp" }, @@ -4913,24 +5094,22 @@ wheels = [ [[package]] name = "nvidia-resiliency-ext" -version = "0.6.0.dev33+15a8515" -source = { git = "https://github.com/NVIDIA/nvidia-resiliency-ext.git?rev=15a851565a4ce846c04431ecb0cf09903ab4837e#15a851565a4ce846c04431ecb0cf09903ab4837e" } +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "defusedxml" }, - { name = "grpcio" }, - { name = "grpcio-tools" }, - { name = "langchain-nvidia-ai-endpoints" }, - { name = "logsage" }, - { name = "mcp" }, { name = "nvidia-ml-py" }, { name = "packaging" }, - { name = "protobuf" }, { name = "psutil" }, + { name = "pynvml" }, { name = "pyyaml" }, - { name = "setproctitle" }, { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "sys_platform != 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/05/38d491962273c7905708762279f440520eb79f3c00b67a023497215ad023/nvidia_resiliency_ext-0.4.1-cp312-cp312-manylinux_2_31_aarch64.whl", hash = "sha256:b3bd5f01535574b16d0f38bca6e39afe3806c4a2896eee1b321cd944e00025a7", size = 444570, upload-time = "2025-07-17T03:50:58.877Z" }, + { url = "https://files.pythonhosted.org/packages/18/8b/4cb8aa2bbdf3705d3034c3f3dacdadb03b3b7dd3dc7f5200e64663fb477f/nvidia_resiliency_ext-0.4.1-cp312-cp312-manylinux_2_31_x86_64.whl", hash = "sha256:ca9f8de465af345952bedbea53c90c0e2323d88cfd830ded0e806fad91845c0e", size = 450280, upload-time = "2025-07-17T03:49:55.327Z" }, +] [[package]] name = "nvidia-sphinx-theme" @@ -4950,11 +5129,23 @@ version = "0.2.13" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/97/02/b3fd3da4ba51764cfc0e4d2b22d5a61511fa79d825344d4704f8429c0bd6/nvtx-0.2.13.tar.gz", hash = "sha256:9db7ba135168e14e1f038866100bf8ed42d3e00b404e9bc7b6280ee3af828b92", size = 112104, upload-time = "2025-08-05T03:27:16.383Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/73/ad21e09dc2534f1e9723bbe5871fa5f03361ac51ca4d411fea6f765b5b6a/nvtx-0.2.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3435cbbffa132f6aaba3abdb01e71a1b961a20858b4cb791883895a25b9305d6", size = 539358, upload-time = "2025-08-04T19:33:16.494Z" }, + { url = "https://files.pythonhosted.org/packages/12/ab/762da984e7671f7c34ae87e5b70523c3eeb4563759268bfaea07c97f32a6/nvtx-0.2.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:453d838dd1424a04303281ee57a73e2b8dca0e03039bc609a945861b8fe7d7d9", size = 545588, upload-time = "2025-08-04T19:37:40.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b6/55bc5916386db70b93cbf543b1e880ead786d9ff0cdcfa262f5a2af46c74/nvtx-0.2.13-cp312-cp312-win_amd64.whl", hash = "sha256:0722d743e0e41e1fb866ebe6446e0cd0d268ca8671313f8da4f8c969956b74d3", size = 99123, upload-time = "2025-08-04T19:24:24.391Z" }, { url = "https://files.pythonhosted.org/packages/41/73/98c0669d5f9387a36d56b0e62ea3919124dd8dd7582d896ed1cae2998f57/nvtx-0.2.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1561d2111c698b1b1075899ff9c3fa7ba83603fc27c2e8ef567de6bbbe85ce1", size = 519840, upload-time = "2025-08-04T19:34:00.877Z" }, { url = "https://files.pythonhosted.org/packages/14/4b/21e975997def8a387543ba2bbe227551ad466781c39fc67f37f53555f37e/nvtx-0.2.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:edd7b729ed0211350258a21dd13422f59bc521de2b2fd21feb6c177af492f4e1", size = 524711, upload-time = "2025-08-04T19:38:03.559Z" }, { url = "https://files.pythonhosted.org/packages/21/d7/0ca146afd875f1e02636323840960071f768b5d8ba3e7d37f2ac9192bfd9/nvtx-0.2.13-cp313-cp313-win_amd64.whl", hash = "sha256:f0524bb71443d5a1f19a6409a9a81405fc437e53c5edfc4c44b6f4504ccf46e3", size = 97317, upload-time = "2025-08-04T19:24:46.391Z" }, ] +[[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 = "omegaconf" version = "2.3.0" @@ -4970,34 +5161,30 @@ wheels = [ [[package]] name = "onnx" -version = "1.21.0" +version = "1.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ml-dtypes", version = "0.5.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 's390x' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "ml-dtypes", version = "0.5.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 's390x' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "protobuf" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/93/942d2a0f6a70538eea042ce0445c8aefd46559ad153469986f29a743c01c/onnx-1.21.0.tar.gz", hash = "sha256:4d8b67d0aaec5864c87633188b91cc520877477ec0254eda122bef8be43cd764", size = 12074608, upload-time = "2026-03-27T21:33:36.118Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/60/e56e8ec44ed34006e6d4a73c92a04d9eea6163cc12440e35045aec069175/onnx-1.18.0.tar.gz", hash = "sha256:3d8dbf9e996629131ba3aa1afd1d8239b660d1f830c6688dd7e03157cccd6b9c", size = 12563009, upload-time = "2025-05-12T22:03:09.626Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/ae/cb644ec84c25e63575d9d8790fdcc5d1a11d67d3f62f872edb35fa38d158/onnx-1.21.0-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:fc2635400fe39ff37ebc4e75342cc54450eadadf39c540ff132c319bf4960095", size = 17965930, upload-time = "2026-03-27T21:32:48.089Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b6/eeb5903586645ef8a49b4b7892580438741acc3df91d7a5bd0f3a59ea9cb/onnx-1.21.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9003d5206c01fa2ff4b46311566865d8e493e1a6998d4009ec6de39843f1b59b", size = 17531344, upload-time = "2026-03-27T21:32:50.837Z" }, - { url = "https://files.pythonhosted.org/packages/a7/00/4823f06357892d1e60d6f34e7299d2ba4ed2108c487cc394f7ce85a3ff14/onnx-1.21.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9261bd580fb8548c9c37b3c6750387eb8f21ea43c63880d37b2c622e1684285", size = 17613697, upload-time = "2026-03-27T21:32:54.222Z" }, - { url = "https://files.pythonhosted.org/packages/23/1d/391f3c567ae068c8ac4f1d1316bae97c9eb45e702f05975fe0e17ad441f0/onnx-1.21.0-cp312-abi3-win32.whl", hash = "sha256:9ea4e824964082811938a9250451d89c4ec474fe42dd36c038bfa5df31993d1e", size = 16287200, upload-time = "2026-03-27T21:32:57.277Z" }, - { url = "https://files.pythonhosted.org/packages/9c/a6/5eefbe5b40ea96de95a766bd2e0e751f35bdea2d4b951991ec9afaa69531/onnx-1.21.0-cp312-abi3-win_amd64.whl", hash = "sha256:458d91948ad9a7729a347550553b49ab6939f9af2cddf334e2116e45467dc61f", size = 16441045, upload-time = "2026-03-27T21:33:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/63/c4/0ed8dc037a39113d2a4d66e0005e07751c299c46b993f1ad5c2c35664c20/onnx-1.21.0-cp312-abi3-win_arm64.whl", hash = "sha256:ca14bc4842fccc3187eb538f07eabeb25a779b39388b006db4356c07403a7bbb", size = 16403134, upload-time = "2026-03-27T21:33:03.987Z" }, - { url = "https://files.pythonhosted.org/packages/f8/89/0e1a9beb536401e2f45ac88735e123f2735e12fc7b56ff6c11727e097526/onnx-1.21.0-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:257d1d1deb6a652913698f1e3f33ef1ca0aa69174892fe38946d4572d89dd94f", size = 17975430, upload-time = "2026-03-27T21:33:07.005Z" }, - { url = "https://files.pythonhosted.org/packages/ec/46/e6dc71a7b3b317265591b20a5f71d0ff5c0d26c24e52283139dc90c66038/onnx-1.21.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7cd7cb8f6459311bdb557cbf6c0ccc6d8ace11c304d1bba0a30b4a4688e245f8", size = 17537435, upload-time = "2026-03-27T21:33:09.765Z" }, - { url = "https://files.pythonhosted.org/packages/49/2e/27affcac63eaf2ef183a44fd1a1354b11da64a6c72fe6f3fdcf5571bcee5/onnx-1.21.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b58a4cfec8d9311b73dc083e4c1fa362069267881144c05139b3eba5dc3a840", size = 17617687, upload-time = "2026-03-27T21:33:12.619Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5c/ac8ed15e941593a3672ce424280b764979026317811f2e8508432bfc3429/onnx-1.21.0-cp313-cp313t-win_amd64.whl", hash = "sha256:1a9baf882562c4cebf79589bebb7cd71a20e30b51158cac3e3bbaf27da6163bd", size = 16449402, upload-time = "2026-03-27T21:33:15.555Z" }, - { url = "https://files.pythonhosted.org/packages/0e/aa/d2231e0dcaad838217afc64c306c8152a080134d2034e247cc973d577674/onnx-1.21.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bba12181566acf49b35875838eba49536a327b2944664b17125577d230c637ad", size = 16408273, upload-time = "2026-03-27T21:33:18.599Z" }, - { url = "https://files.pythonhosted.org/packages/bf/0a/8905b14694def6ad23edf1011fdd581500384062f8c4c567e114be7aa272/onnx-1.21.0-cp314-cp314t-macosx_12_0_universal2.whl", hash = "sha256:7ee9d8fd6a4874a5fa8b44bbcabea104ce752b20469b88bc50c7dcf9030779ad", size = 17975331, upload-time = "2026-03-27T21:33:21.69Z" }, - { url = "https://files.pythonhosted.org/packages/61/28/f4e401e5199d1b9c8b76c7e7ae1169e050515258e877b58fa8bb49d3bdcc/onnx-1.21.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5489f25fe461e7f32128218251a466cabbeeaf1eaa791c79daebf1a80d5a2cc9", size = 17537430, upload-time = "2026-03-27T21:33:24.547Z" }, - { url = "https://files.pythonhosted.org/packages/cf/cf/5d13320eb3660d5af360ea3b43aa9c63a70c92a9b4d1ea0d34501a32fcb8/onnx-1.21.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db17fc0fec46180b6acbd1d5d8650a04e5527c02b09381da0b5b888d02a204c8", size = 17617662, upload-time = "2026-03-27T21:33:27.418Z" }, - { url = "https://files.pythonhosted.org/packages/4d/50/3eaa1878338247be021e6423696813d61e77e534dccbd15a703a144e703d/onnx-1.21.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19d9971a3e52a12968ae6c70fd0f86c349536de0b0c33922ecdbe52d1972fe60", size = 16463688, upload-time = "2026-03-27T21:33:30.229Z" }, - { url = "https://files.pythonhosted.org/packages/a7/48/38d46b43bbb525e0b6a4c2c4204cc6795d67e45687a2f7403e06d8e7053d/onnx-1.21.0-cp314-cp314t-win_arm64.whl", hash = "sha256:efba467efb316baf2a9452d892c2f982b9b758c778d23e38c7f44fa211b30bb9", size = 16423387, upload-time = "2026-03-27T21:33:33.446Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fe/16228aca685392a7114625b89aae98b2dc4058a47f0f467a376745efe8d0/onnx-1.18.0-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:521bac578448667cbb37c50bf05b53c301243ede8233029555239930996a625b", size = 18285770, upload-time = "2025-05-12T22:02:26.116Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/ba50a903a9b5e6f9be0fa50f59eb2fca4a26ee653375408fbc72c3acbf9f/onnx-1.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4da451bf1c5ae381f32d430004a89f0405bc57a8471b0bddb6325a5b334aa40", size = 17421291, upload-time = "2025-05-12T22:02:29.645Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/25ec2ba723ac62b99e8fed6d7b59094dadb15e38d4c007331cc9ae3dfa5f/onnx-1.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99afac90b4cdb1471432203c3c1f74e16549c526df27056d39f41a9a47cfb4af", size = 17584084, upload-time = "2025-05-12T22:02:32.789Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4d/2c253a36070fb43f340ff1d2c450df6a9ef50b938adcd105693fee43c4ee/onnx-1.18.0-cp312-cp312-win32.whl", hash = "sha256:ee159b41a3ae58d9c7341cf432fc74b96aaf50bd7bb1160029f657b40dc69715", size = 15734892, upload-time = "2025-05-12T22:02:35.527Z" }, + { url = "https://files.pythonhosted.org/packages/e8/92/048ba8fafe6b2b9a268ec2fb80def7e66c0b32ab2cae74de886981f05a27/onnx-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:102c04edc76b16e9dfeda5a64c1fccd7d3d2913b1544750c01d38f1ac3c04e05", size = 15850336, upload-time = "2025-05-12T22:02:38.545Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/bbc4ffedd44165dcc407a51ea4c592802a5391ce3dc94aa5045350f64635/onnx-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:911b37d724a5d97396f3c2ef9ea25361c55cbc9aa18d75b12a52b620b67145af", size = 15823802, upload-time = "2025-05-12T22:02:42.037Z" }, + { url = "https://files.pythonhosted.org/packages/45/da/9fb8824513fae836239276870bfcc433fa2298d34ed282c3a47d3962561b/onnx-1.18.0-cp313-cp313-macosx_12_0_universal2.whl", hash = "sha256:030d9f5f878c5f4c0ff70a4545b90d7812cd6bfe511de2f3e469d3669c8cff95", size = 18285906, upload-time = "2025-05-12T22:02:45.01Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/762b5fb5ed1a2b8e9a4bc5e668c82723b1b789c23b74e6b5a3356731ae4e/onnx-1.18.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8521544987d713941ee1e591520044d35e702f73dc87e91e6d4b15a064ae813d", size = 17421486, upload-time = "2025-05-12T22:02:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/12/bb/471da68df0364f22296456c7f6becebe0a3da1ba435cdb371099f516da6e/onnx-1.18.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c137eecf6bc618c2f9398bcc381474b55c817237992b169dfe728e169549e8f", size = 17583581, upload-time = "2025-05-12T22:02:51.784Z" }, + { url = "https://files.pythonhosted.org/packages/76/0d/01a95edc2cef6ad916e04e8e1267a9286f15b55c90cce5d3cdeb359d75d6/onnx-1.18.0-cp313-cp313-win32.whl", hash = "sha256:6c093ffc593e07f7e33862824eab9225f86aa189c048dd43ffde207d7041a55f", size = 15734621, upload-time = "2025-05-12T22:02:54.62Z" }, + { url = "https://files.pythonhosted.org/packages/64/95/253451a751be32b6173a648b68f407188009afa45cd6388780c330ff5d5d/onnx-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:230b0fb615e5b798dc4a3718999ec1828360bc71274abd14f915135eab0255f1", size = 15850472, upload-time = "2025-05-12T22:02:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b1/6fd41b026836df480a21687076e0f559bc3ceeac90f2be8c64b4a7a1f332/onnx-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:6f91930c1a284135db0f891695a263fc876466bf2afbd2215834ac08f600cfca", size = 15823808, upload-time = "2025-05-12T22:03:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/f3/499e53dd41fa7302f914dd18543da01e0786a58b9a9d347497231192001f/onnx-1.18.0-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:2f4d37b0b5c96a873887652d1cbf3f3c70821b8c66302d84b0f0d89dd6e47653", size = 18316526, upload-time = "2025-05-12T22:03:03.691Z" }, + { url = "https://files.pythonhosted.org/packages/84/dd/6abe5d7bd23f5ed3ade8352abf30dff1c7a9e97fc1b0a17b5d7c726e98a9/onnx-1.18.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a69afd0baa372162948b52c13f3aa2730123381edf926d7ef3f68ca7cec6d0d0", size = 15865055, upload-time = "2025-05-12T22:03:06.663Z" }, ] [[package]] @@ -5005,10 +5192,9 @@ name = "onnx-ir" version = "0.1.16" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ml-dtypes", version = "0.5.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 's390x' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "ml-dtypes", version = "0.5.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 's390x' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "ml-dtypes" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "onnx" }, { name = "typing-extensions" }, ] @@ -5022,10 +5208,9 @@ name = "onnxscript" version = "0.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ml-dtypes", version = "0.5.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 's390x' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "ml-dtypes", version = "0.5.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 's390x' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "ml-dtypes" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "onnx" }, { name = "onnx-ir" }, { name = "packaging" }, @@ -5134,7 +5319,7 @@ name = "opencv-python-headless" version = "4.11.0.86" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/36/2f/5b2b3ba52c864848885ba988f24b7f105052f68da9ab0e693cc7c25b0b30/opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798", size = 95177929, upload-time = "2025-01-16T13:53:40.22Z" } @@ -5289,61 +5474,106 @@ wheels = [ [[package]] name = "orjson" -version = "3.11.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" }, - { url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" }, - { url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" }, - { url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bb/a6c55896197f97b6d4b4e7c7fd77e7235517c34f5d6ad5aadd43c54c6d7c/orjson-3.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ab359aff0436d80bfe8a23b46b5fea69f1e18aaf1760a709b4787f1318b317f", size = 135521, upload-time = "2026-03-31T16:15:42.758Z" }, - { url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" }, - { url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" }, - { url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" }, - { url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" }, - { url = "https://files.pythonhosted.org/packages/00/a3/ecfe62434096f8a794d4976728cb59bcfc4a643977f21c2040545d37eb4c/orjson-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98bdc6cb889d19bed01de46e67574a2eab61f5cc6b768ed50e8ac68e9d6ffab6", size = 147801, upload-time = "2026-03-31T16:15:52.939Z" }, - { url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" }, - { url = "https://files.pythonhosted.org/packages/01/d6/6dde4f31842d87099238f1f07b459d24edc1a774d20687187443ab044191/orjson-3.11.8-cp313-cp313-win32.whl", hash = "sha256:01c4e5a6695dc09098f2e6468a251bc4671c50922d4d745aff1a0a33a0cf5b8d", size = 131956, upload-time = "2026-03-31T16:15:56.081Z" }, - { url = "https://files.pythonhosted.org/packages/c1/f9/4e494a56e013db957fb77186b818b916d4695b8fa2aa612364974160e91b/orjson-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:c154a35dd1330707450bb4d4e7dd1f17fa6f42267a40c1e8a1daa5e13719b4b8", size = 127410, upload-time = "2026-03-31T16:15:57.54Z" }, - { url = "https://files.pythonhosted.org/packages/57/7f/803203d00d6edb6e9e7eef421d4e1adbb5ea973e40b3533f3cfd9aeb374e/orjson-3.11.8-cp313-cp313-win_arm64.whl", hash = "sha256:4861bde57f4d253ab041e374f44023460e60e71efaa121f3c5f0ed457c3a701e", size = 127338, upload-time = "2026-03-31T16:15:59.106Z" }, - { url = "https://files.pythonhosted.org/packages/6d/35/b01910c3d6b85dc882442afe5060cbf719c7d1fc85749294beda23d17873/orjson-3.11.8-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec795530a73c269a55130498842aaa762e4a939f6ce481a7e986eeaa790e9da4", size = 229171, upload-time = "2026-03-31T16:16:00.651Z" }, - { url = "https://files.pythonhosted.org/packages/c2/56/c9ec97bd11240abef39b9e5d99a15462809c45f677420fd148a6c5e6295e/orjson-3.11.8-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c492a0e011c0f9066e9ceaa896fbc5b068c54d365fea5f3444b697ee01bc8625", size = 128746, upload-time = "2026-03-31T16:16:02.673Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e4/66d4f30a90de45e2f0cbd9623588e8ae71eef7679dbe2ae954ed6d66a41f/orjson-3.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:883206d55b1bd5f5679ad5e6ddd3d1a5e3cac5190482927fdb8c78fb699193b5", size = 131867, upload-time = "2026-03-31T16:16:04.342Z" }, - { url = "https://files.pythonhosted.org/packages/19/30/2a645fc9286b928675e43fa2a3a16fb7b6764aa78cc719dc82141e00f30b/orjson-3.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5774c1fdcc98b2259800b683b19599c133baeb11d60033e2095fd9d4667b82db", size = 124664, upload-time = "2026-03-31T16:16:05.837Z" }, - { url = "https://files.pythonhosted.org/packages/db/44/77b9a86d84a28d52ba3316d77737f6514e17118119ade3f91b639e859029/orjson-3.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7381c83dd3d4a6347e6635950aa448f54e7b8406a27c7ecb4a37e9f1ae08b", size = 129701, upload-time = "2026-03-31T16:16:07.407Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ea/eff3d9bfe47e9bc6969c9181c58d9f71237f923f9c86a2d2f490cd898c82/orjson-3.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14439063aebcb92401c11afc68ee4e407258d2752e62d748b6942dad20d2a70d", size = 141202, upload-time = "2026-03-31T16:16:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/52/c8/90d4b4c60c84d62068d0cf9e4d8f0a4e05e76971d133ac0c60d818d4db20/orjson-3.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa72e71977bff96567b0f500fc5bfd2fdf915f34052c782a4c6ebbdaa97aa858", size = 127194, upload-time = "2026-03-31T16:16:11.02Z" }, - { url = "https://files.pythonhosted.org/packages/8d/c7/ea9e08d1f0ba981adffb629811148b44774d935171e7b3d780ae43c4c254/orjson-3.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7679bc2f01bb0d219758f1a5f87bb7c8a81c0a186824a393b366876b4948e14f", size = 133639, upload-time = "2026-03-31T16:16:13.434Z" }, - { url = "https://files.pythonhosted.org/packages/6c/8c/ddbbfd6ba59453c8fc7fe1d0e5983895864e264c37481b2a791db635f046/orjson-3.11.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14f7b8fcb35ef403b42fa5ecfa4ed032332a91f3dc7368fbce4184d59e1eae0d", size = 141914, upload-time = "2026-03-31T16:16:14.955Z" }, - { url = "https://files.pythonhosted.org/packages/4e/31/dbfbefec9df060d34ef4962cd0afcb6fa7a9ec65884cb78f04a7859526c3/orjson-3.11.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c2bdf7b2facc80b5e34f48a2d557727d5c5c57a8a450de122ae81fa26a81c1bc", size = 423800, upload-time = "2026-03-31T16:16:16.594Z" }, - { url = "https://files.pythonhosted.org/packages/87/cf/f74e9ae9803d4ab46b163494adba636c6d7ea955af5cc23b8aaa94cfd528/orjson-3.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ccd7ba1b0605813a0715171d39ec4c314cb97a9c85893c2c5c0c3a3729df38bf", size = 147837, upload-time = "2026-03-31T16:16:18.585Z" }, - { url = "https://files.pythonhosted.org/packages/64/e6/9214f017b5db85e84e68602792f742e5dc5249e963503d1b356bee611e01/orjson-3.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbc8c9c02463fef4d3c53a9ba3336d05496ec8e1f1c53326a1e4acc11f5c600", size = 136441, upload-time = "2026-03-31T16:16:20.151Z" }, - { url = "https://files.pythonhosted.org/packages/24/dd/3590348818f58f837a75fb969b04cdf187ae197e14d60b5e5a794a38b79d/orjson-3.11.8-cp314-cp314-win32.whl", hash = "sha256:0b57f67710a8cd459e4e54eb96d5f77f3624eba0c661ba19a525807e42eccade", size = 131983, upload-time = "2026-03-31T16:16:21.823Z" }, - { url = "https://files.pythonhosted.org/packages/3f/0f/b6cb692116e05d058f31ceee819c70f097fa9167c82f67fabe7516289abc/orjson-3.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:735e2262363dcbe05c35e3a8869898022af78f89dde9e256924dc02e99fe69ca", size = 127396, upload-time = "2026-03-31T16:16:23.685Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d1/facb5b5051fabb0ef9d26c6544d87ef19a939a9a001198655d0d891062dd/orjson-3.11.8-cp314-cp314-win_arm64.whl", hash = "sha256:6ccdea2c213cf9f3d9490cbd5d427693c870753df41e6cb375bd79bcbafc8817", size = 127330, upload-time = "2026-03-31T16:16:25.496Z" }, +version = "3.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/4d/8df5f83256a809c22c4d6792ce8d43bb503be0fb7a8e4da9025754b09658/orjson-3.11.3.tar.gz", hash = "sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a", size = 5482394, upload-time = "2025-08-26T17:46:43.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/b0/a7edab2a00cdcb2688e1c943401cb3236323e7bfd2839815c6131a3742f4/orjson-3.11.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b", size = 238259, upload-time = "2025-08-26T17:45:15.093Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c6/ff4865a9cc398a07a83342713b5932e4dc3cb4bf4bc04e8f83dedfc0d736/orjson-3.11.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2", size = 127633, upload-time = "2025-08-26T17:45:16.417Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e6/e00bea2d9472f44fe8794f523e548ce0ad51eb9693cf538a753a27b8bda4/orjson-3.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a", size = 123061, upload-time = "2025-08-26T17:45:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/31/9fbb78b8e1eb3ac605467cb846e1c08d0588506028b37f4ee21f978a51d4/orjson-3.11.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c", size = 127956, upload-time = "2025-08-26T17:45:19.172Z" }, + { url = "https://files.pythonhosted.org/packages/36/88/b0604c22af1eed9f98d709a96302006915cfd724a7ebd27d6dd11c22d80b/orjson-3.11.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064", size = 130790, upload-time = "2025-08-26T17:45:20.586Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/1c1238ae9fffbfed51ba1e507731b3faaf6b846126a47e9649222b0fd06f/orjson-3.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424", size = 132385, upload-time = "2025-08-26T17:45:22.036Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b5/c06f1b090a1c875f337e21dd71943bc9d84087f7cdf8c6e9086902c34e42/orjson-3.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23", size = 135305, upload-time = "2025-08-26T17:45:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/a0/26/5f028c7d81ad2ebbf84414ba6d6c9cac03f22f5cd0d01eb40fb2d6a06b07/orjson-3.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667", size = 132875, upload-time = "2025-08-26T17:45:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/b8df70d9cfb56e385bf39b4e915298f9ae6c61454c8154a0f5fd7efcd42e/orjson-3.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f", size = 130940, upload-time = "2025-08-26T17:45:27.209Z" }, + { url = "https://files.pythonhosted.org/packages/da/5e/afe6a052ebc1a4741c792dd96e9f65bf3939d2094e8b356503b68d48f9f5/orjson-3.11.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1", size = 403852, upload-time = "2025-08-26T17:45:28.478Z" }, + { url = "https://files.pythonhosted.org/packages/f8/90/7bbabafeb2ce65915e9247f14a56b29c9334003536009ef5b122783fe67e/orjson-3.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc", size = 146293, upload-time = "2025-08-26T17:45:29.86Z" }, + { url = "https://files.pythonhosted.org/packages/27/b3/2d703946447da8b093350570644a663df69448c9d9330e5f1d9cce997f20/orjson-3.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049", size = 135470, upload-time = "2025-08-26T17:45:31.243Z" }, + { url = "https://files.pythonhosted.org/packages/38/70/b14dcfae7aff0e379b0119c8a812f8396678919c431efccc8e8a0263e4d9/orjson-3.11.3-cp312-cp312-win32.whl", hash = "sha256:3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca", size = 136248, upload-time = "2025-08-26T17:45:32.567Z" }, + { url = "https://files.pythonhosted.org/packages/35/b8/9e3127d65de7fff243f7f3e53f59a531bf6bb295ebe5db024c2503cc0726/orjson-3.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1", size = 131437, upload-time = "2025-08-26T17:45:34.949Z" }, + { url = "https://files.pythonhosted.org/packages/51/92/a946e737d4d8a7fd84a606aba96220043dcc7d6988b9e7551f7f6d5ba5ad/orjson-3.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710", size = 125978, upload-time = "2025-08-26T17:45:36.422Z" }, + { url = "https://files.pythonhosted.org/packages/fc/79/8932b27293ad35919571f77cb3693b5906cf14f206ef17546052a241fdf6/orjson-3.11.3-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810", size = 238127, upload-time = "2025-08-26T17:45:38.146Z" }, + { url = "https://files.pythonhosted.org/packages/1c/82/cb93cd8cf132cd7643b30b6c5a56a26c4e780c7a145db6f83de977b540ce/orjson-3.11.3-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43", size = 127494, upload-time = "2025-08-26T17:45:39.57Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/2d9eb181a9b6bb71463a78882bcac1027fd29cf62c38a40cc02fc11d3495/orjson-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27", size = 123017, upload-time = "2025-08-26T17:45:40.876Z" }, + { url = "https://files.pythonhosted.org/packages/b4/14/a0e971e72d03b509190232356d54c0f34507a05050bd026b8db2bf2c192c/orjson-3.11.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f", size = 127898, upload-time = "2025-08-26T17:45:42.188Z" }, + { url = "https://files.pythonhosted.org/packages/8e/af/dc74536722b03d65e17042cc30ae586161093e5b1f29bccda24765a6ae47/orjson-3.11.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c", size = 130742, upload-time = "2025-08-26T17:45:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/7a3b63b6677bce089fe939353cda24a7679825c43a24e49f757805fc0d8a/orjson-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be", size = 132377, upload-time = "2025-08-26T17:45:45.525Z" }, + { url = "https://files.pythonhosted.org/packages/fc/cd/ce2ab93e2e7eaf518f0fd15e3068b8c43216c8a44ed82ac2b79ce5cef72d/orjson-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d", size = 135313, upload-time = "2025-08-26T17:45:46.821Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b4/f98355eff0bd1a38454209bbc73372ce351ba29933cb3e2eba16c04b9448/orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2", size = 132908, upload-time = "2025-08-26T17:45:48.126Z" }, + { url = "https://files.pythonhosted.org/packages/eb/92/8f5182d7bc2a1bed46ed960b61a39af8389f0ad476120cd99e67182bfb6d/orjson-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f", size = 130905, upload-time = "2025-08-26T17:45:49.414Z" }, + { url = "https://files.pythonhosted.org/packages/1a/60/c41ca753ce9ffe3d0f67b9b4c093bdd6e5fdb1bc53064f992f66bb99954d/orjson-3.11.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee", size = 403812, upload-time = "2025-08-26T17:45:51.085Z" }, + { url = "https://files.pythonhosted.org/packages/dd/13/e4a4f16d71ce1868860db59092e78782c67082a8f1dc06a3788aef2b41bc/orjson-3.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e", size = 146277, upload-time = "2025-08-26T17:45:52.851Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8b/bafb7f0afef9344754a3a0597a12442f1b85a048b82108ef2c956f53babd/orjson-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633", size = 135418, upload-time = "2025-08-26T17:45:54.806Z" }, + { url = "https://files.pythonhosted.org/packages/60/d4/bae8e4f26afb2c23bea69d2f6d566132584d1c3a5fe89ee8c17b718cab67/orjson-3.11.3-cp313-cp313-win32.whl", hash = "sha256:2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b", size = 136216, upload-time = "2025-08-26T17:45:57.182Z" }, + { url = "https://files.pythonhosted.org/packages/88/76/224985d9f127e121c8cad882cea55f0ebe39f97925de040b75ccd4b33999/orjson-3.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae", size = 131362, upload-time = "2025-08-26T17:45:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cf/0dce7a0be94bd36d1346be5067ed65ded6adb795fdbe3abd234c8d576d01/orjson-3.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce", size = 125989, upload-time = "2025-08-26T17:45:59.95Z" }, + { url = "https://files.pythonhosted.org/packages/ef/77/d3b1fef1fc6aaeed4cbf3be2b480114035f4df8fa1a99d2dac1d40d6e924/orjson-3.11.3-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4", size = 238115, upload-time = "2025-08-26T17:46:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/468d21d49bb12f900052edcfbf52c292022d0a323d7828dc6376e6319703/orjson-3.11.3-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e", size = 127493, upload-time = "2025-08-26T17:46:03.466Z" }, + { url = "https://files.pythonhosted.org/packages/67/46/1e2588700d354aacdf9e12cc2d98131fb8ac6f31ca65997bef3863edb8ff/orjson-3.11.3-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d", size = 122998, upload-time = "2025-08-26T17:46:04.803Z" }, + { url = "https://files.pythonhosted.org/packages/3b/94/11137c9b6adb3779f1b34fd98be51608a14b430dbc02c6d41134fbba484c/orjson-3.11.3-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229", size = 132915, upload-time = "2025-08-26T17:46:06.237Z" }, + { url = "https://files.pythonhosted.org/packages/10/61/dccedcf9e9bcaac09fdabe9eaee0311ca92115699500efbd31950d878833/orjson-3.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451", size = 130907, upload-time = "2025-08-26T17:46:07.581Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/0e935539aa7b08b3ca0f817d73034f7eb506792aae5ecc3b7c6e679cdf5f/orjson-3.11.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167", size = 403852, upload-time = "2025-08-26T17:46:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2b/50ae1a5505cd1043379132fdb2adb8a05f37b3e1ebffe94a5073321966fd/orjson-3.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077", size = 146309, upload-time = "2025-08-26T17:46:10.576Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1d/a473c158e380ef6f32753b5f39a69028b25ec5be331c2049a2201bde2e19/orjson-3.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872", size = 135424, upload-time = "2025-08-26T17:46:12.386Z" }, + { url = "https://files.pythonhosted.org/packages/da/09/17d9d2b60592890ff7382e591aa1d9afb202a266b180c3d4049b1ec70e4a/orjson-3.11.3-cp314-cp314-win32.whl", hash = "sha256:0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d", size = 136266, upload-time = "2025-08-26T17:46:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/15/58/358f6846410a6b4958b74734727e582ed971e13d335d6c7ce3e47730493e/orjson-3.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804", size = 131351, upload-time = "2025-08-26T17:46:15.27Z" }, + { url = "https://files.pythonhosted.org/packages/28/01/d6b274a0635be0468d4dbd9cafe80c47105937a0d42434e805e67cd2ed8b/orjson-3.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc", size = 125985, upload-time = "2025-08-26T17:46:16.67Z" }, ] [[package]] name = "outlines" -version = "1.2.12" +version = "0.1.11" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "airportsdata" }, { name = "cloudpickle" }, { name = "diskcache" }, - { name = "genson" }, + { name = "interegular" }, { name = "jinja2" }, - { name = "jsonpath-ng" }, { name = "jsonschema" }, - { name = "outlines-core", version = "0.2.14", source = { registry = "https://pypi.org/simple" } }, - { name = "pillow" }, + { name = "lark" }, + { name = "nest-asyncio" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" } }, + { name = "outlines-core", version = "0.1.26", source = { registry = "https://pypi.org/simple" } }, + { name = "pycountry" }, { name = "pydantic" }, + { name = "referencing" }, + { name = "requests" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/75b7484ab98597cadeee383488b9cbfb07365ca1946f348e27d615c94e6a/outlines-1.2.12.tar.gz", hash = "sha256:7aa3a49b8c75e94ec6f6194ff7ab47fcc57bff3a38590590c4003fbba9354fdd", size = 2952569, upload-time = "2026-03-03T11:07:08.839Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/d0/d59ae830bf7026425942899e3d48e77b58a713cff946a695e5405808da1b/outlines-0.1.11.tar.gz", hash = "sha256:0997bd9da1cc050e430bd08995dc7d4bd855918bafa4531e49d3f37110a23aba", size = 2488858, upload-time = "2024-12-13T07:24:08.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/c4/d2e66cad1965afbd0e679dd10b02d03129322438a91ecf9e71b84d2a185f/outlines-1.2.12-py3-none-any.whl", hash = "sha256:d186c5b8451ff18f0e5637f28841ef1fdce815e861863ce91055d88313dbc482", size = 102309, upload-time = "2026-03-03T11:07:07.364Z" }, + { url = "https://files.pythonhosted.org/packages/13/b4/99ea4a122bef60e3fd6402d19665aff1f928e0daf8fac3044d0b73f72003/outlines-0.1.11-py3-none-any.whl", hash = "sha256:f5a5f2242ed9802d3aab7a92789bf4008d734c576be9258cc0a297f690124727", size = 87623, upload-time = "2024-12-13T07:24:05.817Z" }, +] + +[[package]] +name = "outlines-core" +version = "0.1.26" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version < '3.13' and sys_platform == 'linux'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version < '3.13' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "interegular" }, + { name = "jsonschema" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/f3/274d07f4702728b43581235a77e545ec602b25f9b0098b288a0f3052521d/outlines_core-0.1.26.tar.gz", hash = "sha256:481c4301341e77cc8f1832d616784adb4d461b4fec65878e7c0d2cba7163a189", size = 75139, upload-time = "2024-12-12T23:38:50.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/86/0fb40746e579db38d89f127122a3900d9e0350f76aae8cb61adeaff44cc2/outlines_core-0.1.26-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f54633bca50055d42ea4d94ae06dcbe52d3d76a9b621b75723b1177d0d952953", size = 321874, upload-time = "2024-12-12T23:38:26.834Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0c/b91f7bc03843796c1d643ee030b6cd8fd5a8ba2cd4856c855f140c878976/outlines_core-0.1.26-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9525321b48700dcaaabf60bcdc951e45f9357ba3fb3e1bfc81b662d7d4170e7c", size = 301995, upload-time = "2024-12-12T23:38:29.625Z" }, + { url = "https://files.pythonhosted.org/packages/ad/db/fa91a2d54288b900de82d86eda3adb2417b3b5b2db6256854a5e8bc85c32/outlines_core-0.1.26-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00f409f72c11f6ffadb57066950dd384d5388015028c1a1a615c9a64988dae3e", size = 321050, upload-time = "2024-12-12T23:38:32.274Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1d/a36292b6198986bd9c3ff8c24355deb82ed5475403379ee40b5b5473e2e3/outlines_core-0.1.26-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e86a1bb46adc5cbf6dfd7a7fe4105e0e2a4c6e041732a053126b41c521a1f223", size = 343201, upload-time = "2024-12-12T23:38:34.631Z" }, + { url = "https://files.pythonhosted.org/packages/08/63/5dd2b5a364412f674b6edcb59b0c21513bdb07cdcc7613b064c1a0660d01/outlines_core-0.1.26-cp312-cp312-win32.whl", hash = "sha256:19f462f6b00935708677ad27cb4df55e0e17f6ffe713ab750f5f2683b090f95d", size = 233970, upload-time = "2024-12-12T23:38:37.318Z" }, + { url = "https://files.pythonhosted.org/packages/a5/56/8adf0b7446d1e975c2314454813c59eb7b195889908a2932ed34148c113c/outlines_core-0.1.26-cp312-cp312-win_amd64.whl", hash = "sha256:9b36bff12779e58883747116893a17b3551bbd10865878b951b03a44d112229a", size = 243578, upload-time = "2024-12-12T23:38:39.964Z" }, ] [[package]] @@ -5351,16 +5581,25 @@ name = "outlines-core" version = "0.2.11" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "platform_machine == 'aarch64' and sys_platform == 'linux'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux'", - "platform_machine == 's390x' and sys_platform == 'linux'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux'", - "platform_machine != 's390x' and sys_platform == 'darwin'", - "platform_machine == 's390x' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version < '3.13' and sys_platform == 'darwin'", ] sdist = { url = "https://files.pythonhosted.org/packages/1a/d3/e04e9145f8f806723dec9b9e5227ad695a3efcd3ced7794cf7c22b15df5e/outlines_core-0.2.11.tar.gz", hash = "sha256:dfce56f717ff5083e54cbcfdb66cad243365437fccbb5509adaa7e31e030f1d8", size = 197263, upload-time = "2025-05-19T10:12:51.719Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/2c/c7636823244c70e2960060bf9bd978248dffb55c5e7c91c46d18354b2a24/outlines_core-0.2.11-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:4a9db4872bae083631d720994f4cee603bce0536b33d5a988814576863b657cf", size = 1957668, upload-time = "2025-05-19T10:12:18.29Z" }, + { url = "https://files.pythonhosted.org/packages/c7/09/5c62047da139d722317a444a4d01cd5f11943a8c2eaecce784341dd0844a/outlines_core-0.2.11-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8359a45c59f6a8f2eb717245806501a59044c75f6ea8bd08faaa131cc8cdec45", size = 2130493, upload-time = "2025-05-19T10:12:19.537Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/d6a2810f90e37d550168e0c0a9a915086ea721444727e3ca2c630898d1ef/outlines_core-0.2.11-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:5d26a46591377340e0b870b8a96ea8341058341a62ee0bded9098e0c88dd24f4", size = 1956804, upload-time = "2025-05-19T10:12:20.755Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ea/339e6c273b5581128c3b7ca27d428d8993c3085912af1a467aa32ef0e9d1/outlines_core-0.2.11-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:ae460a34675fb11d92a5c605a480fbae4cd6c1b2d11b3698da64a7fcaba64dcf", size = 2127085, upload-time = "2025-05-19T10:12:22.02Z" }, + { url = "https://files.pythonhosted.org/packages/92/c7/a65d1fddf49830ebc41422294eacde35286d9f68994a8aa905cb14f5aade/outlines_core-0.2.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86df9740368866295077346440d911df4972da2b3f1f54b8125e6f329e8a8891", size = 2287677, upload-time = "2025-05-19T10:12:24.24Z" }, + { url = "https://files.pythonhosted.org/packages/23/79/8795aed8be9b77dd69d78e7cfbfcf28c179e6b08da6e56bbbf48a09fe55f/outlines_core-0.2.11-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:96ce4dd78f106799be4a0a5795cefd1352806162973756a4b6fce4bb6eddd7e4", size = 2113000, upload-time = "2025-05-19T10:12:25.446Z" }, + { url = "https://files.pythonhosted.org/packages/59/e3/cbe9294b06d92ee1892dbb6f2125d833d68e8629d45d080d6daba54eec2d/outlines_core-0.2.11-cp312-cp312-win32.whl", hash = "sha256:358db161cce3650ba822e118dcf0a1efa571c7deb4864ab9d64ca2c9cca7425d", size = 1765703, upload-time = "2025-05-19T10:12:26.693Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c9/ed3cf362515fac16e313368b9b2f2497051f4ded88679205830b6f889f54/outlines_core-0.2.11-cp312-cp312-win_amd64.whl", hash = "sha256:231f9d20d2630c70665345821780d7808b29539620a75c99f65113b518c51032", size = 2060945, upload-time = "2025-05-19T10:12:28.294Z" }, { url = "https://files.pythonhosted.org/packages/11/58/df6f57546f7792c990a4380ceaf99243a0b26b24c199e34e0a9277c89976/outlines_core-0.2.11-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:0907ff25d79edbf8650268028de85a1b41b38696f147059e007da4626a1031f1", size = 1957172, upload-time = "2025-05-19T10:12:29.737Z" }, { url = "https://files.pythonhosted.org/packages/9b/cf/b07e33c44544e7865ec481554788807dfa6ad10fd86191ad21f2200f145e/outlines_core-0.2.11-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:f4146da5957f97550eebd19e80635e48035886fd10f03e9735cc111caaf74e93", size = 2130284, upload-time = "2025-05-19T10:12:31.408Z" }, { url = "https://files.pythonhosted.org/packages/83/70/8f981706e2620914c48fd1edb42f9409d76b84c72149d48e89d14820fab6/outlines_core-0.2.11-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:8776a6db8843187c90e4c54bf94510cda68ca7a11c9b48d90587179fd3224bc2", size = 1956727, upload-time = "2025-05-19T10:12:32.996Z" }, @@ -5371,38 +5610,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a5/39/4c07f1d1f8e6ed85db9fe73a021113795a05aae8a84f36f0bdebb08bfde8/outlines_core-0.2.11-cp313-cp313-win_amd64.whl", hash = "sha256:ad46698564c9b13cbfbc744067de12be73bd740d7b2de20ec6b979ad7511f7c9", size = 2060567, upload-time = "2025-05-19T10:12:39.228Z" }, ] -[[package]] -name = "outlines-core" -version = "0.2.14" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "platform_machine != 's390x' and sys_platform == 'linux'", - "platform_machine == 's390x' and sys_platform == 'linux'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux'", - "platform_machine != 's390x' and sys_platform == 'darwin'", - "platform_machine == 's390x' and sys_platform == 'darwin'", -] -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/13/9d/e6c81c975c123f0639d5f6909c987e510d43e07c2e1e6495b21639c4dec6/outlines_core-0.2.14-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8b3e8d668188282a1f7666732bb8a01958ab134db35bb792e7442a40e55ff1e7", size = 2049297, upload-time = "2026-01-09T15:58:39.184Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d1/5ce55ef724aed0915edc877b6dd610d39b3169e4341154bb53daa022065a/outlines_core-0.2.14-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:66e695b375b180725fb534d9adf298531c152ec3d881e3b9e01c82b5dd269f52", size = 2200944, upload-time = "2026-01-09T15:58:40.257Z" }, - { url = "https://files.pythonhosted.org/packages/32/e3/60ad781251eedcf1496317ecd58eb2e4488717ba63b10494ab49dfd05e5d/outlines_core-0.2.14-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:6bd166d3b07acef2f60d4ede44592a26d3f7d8712876bfc8e22150045def5857", size = 2049607, upload-time = "2026-01-09T15:58:41.635Z" }, - { url = "https://files.pythonhosted.org/packages/bc/2d/662d6a76face5b4b3481f888900d00856c37aa2927341a023866457da212/outlines_core-0.2.14-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:9d45462d7548aa0e17176a691ae73447f3e6bed9658a0cd96fe72eadf7474475", size = 2197755, upload-time = "2026-01-09T15:58:42.861Z" }, - { url = "https://files.pythonhosted.org/packages/c1/9a/4b62903de006d991b58674ff033c1b6fb92be5767360376fc961f6771bdb/outlines_core-0.2.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6453e23f01d98ec48e3a4141d7112792ce77001dfb28d91d6fd89f47009f91ef", size = 2341051, upload-time = "2026-01-09T15:58:44.415Z" }, - { url = "https://files.pythonhosted.org/packages/50/36/1532f7d9ab16c676812d94528e89964aa0d15f12adcb285e6ed86f86f2fe/outlines_core-0.2.14-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7deef6df74cb247f2a3a62f03438ba967456504b0555ec7029f8db834e054448", size = 2236778, upload-time = "2026-01-09T15:58:45.437Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5a/dfd94f15f4c04e691e7fdf30cf8b9b22bf2cbc426b3ef270af3e200596d5/outlines_core-0.2.14-cp313-cp313-win32.whl", hash = "sha256:bb008c7ecc034bcfda0ddc10a4d1f2181a4b61ec1643ee56183dd6fa64139c9d", size = 1842727, upload-time = "2026-01-09T15:58:46.723Z" }, - { url = "https://files.pythonhosted.org/packages/34/35/e24ab5d2116812464380587435297d8ece2f0218c2ba8afc9f541e3a6911/outlines_core-0.2.14-cp313-cp313-win_amd64.whl", hash = "sha256:eb27e92204b296a063ac58f361153be4e78c8103a96e0b1c085b22d4fc3534cf", size = 2137108, upload-time = "2026-01-09T15:58:47.784Z" }, - { url = "https://files.pythonhosted.org/packages/1b/28/22fe8ee3bdf9cf13ab88a9d9b96729d9966c791c25227d0b7ca45c8d118f/outlines_core-0.2.14-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:69410e5b55bcbaad8c865d94bd01e7bff8a57996dcd2251b7d50dec70d7d9a63", size = 2050470, upload-time = "2026-01-09T15:58:49.217Z" }, - { url = "https://files.pythonhosted.org/packages/d4/3e/30ce0b13e4c4c82de606c8bbf60775ac6fca1978efa54cd553893795fd0b/outlines_core-0.2.14-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:adf96395759d7fdf6efeb8a67d3f36f520c1546bfd4df0752306db8c7cb7d6c5", size = 2202138, upload-time = "2026-01-09T15:58:50.281Z" }, - { url = "https://files.pythonhosted.org/packages/53/13/bd2ff9e90b28fa0dcc345c9196731ed9126e366733c8ccbc559149e34893/outlines_core-0.2.14-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:b02bb0fc21c5e23e2ff9b2d1459db2c1c3e813a7646c9d5db091c6931edb9c85", size = 2050325, upload-time = "2026-01-09T15:58:51.596Z" }, - { url = "https://files.pythonhosted.org/packages/1e/25/fc0ae7d04345d17267d4dd5c693ed9e86c7f44419cc04ad92348472781be/outlines_core-0.2.14-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:e75395b1cccecdf85d8d8265aba28841ddeb1e8da406f4b1e0135df5a6e9960f", size = 2199081, upload-time = "2026-01-09T15:58:53.17Z" }, - { url = "https://files.pythonhosted.org/packages/d5/63/dfa000239e46f17b47e6dc9bec3aab8a8136fe400312f1916320e02c8f38/outlines_core-0.2.14-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1776ae984574461f249fe590314a439992eb9b883f4091b8fa7fc56f29f3717", size = 2343210, upload-time = "2026-01-09T15:58:54.282Z" }, - { url = "https://files.pythonhosted.org/packages/36/4f/0e63da06c6054f154ef22b5ef3c6b9030cb22da9c03d2d2dd82836a1e795/outlines_core-0.2.14-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:7eba2b41dac03d6e6e8d5ea0aecbbc03dacb4c57de3b1fc944d0bafb022941f7", size = 2238206, upload-time = "2026-01-09T15:58:55.705Z" }, - { url = "https://files.pythonhosted.org/packages/74/4e/382271ab5ffe768055f11dddb50e82a0c46487f3766bf08a06cfcd35388b/outlines_core-0.2.14-cp314-cp314-win32.whl", hash = "sha256:0cd8ce3ce61df44fd9c5450d9744e2280586c2a6e6e3dfefa0dab1944764b424", size = 1845364, upload-time = "2026-01-09T15:58:56.795Z" }, - { url = "https://files.pythonhosted.org/packages/0d/11/13adf2d02c681b599c1eb550b0dbd763d1b818a106a13bd693019bdb5637/outlines_core-0.2.14-cp314-cp314-win_amd64.whl", hash = "sha256:3e67fc23b1a3ac9562488fb50f409c171538b76f64aa5f7e25d9b0bf14770204", size = 2139979, upload-time = "2026-01-09T15:58:57.984Z" }, -] - [[package]] name = "packaging" version = "25.0" @@ -5417,14 +5624,21 @@ name = "pandas" version = "2.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "python-dateutil" }, { name = "pytz" }, { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493, upload-time = "2025-07-07T19:20:04.079Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/46/de/b8445e0f5d217a99fe0eeb2f4988070908979bec3587c0633e5428ab596c/pandas-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:689968e841136f9e542020698ee1c4fbe9caa2ed2213ae2388dc7b81721510d3", size = 11588172, upload-time = "2025-07-07T19:18:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e0/801cdb3564e65a5ac041ab99ea6f1d802a6c325bb6e58c79c06a3f1cd010/pandas-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:025e92411c16cbe5bb2a4abc99732a6b132f439b8aab23a59fa593eb00704232", size = 10717365, upload-time = "2025-07-07T19:18:54.785Z" }, + { url = "https://files.pythonhosted.org/packages/51/a5/c76a8311833c24ae61a376dbf360eb1b1c9247a5d9c1e8b356563b31b80c/pandas-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7ff55f31c4fcb3e316e8f7fa194566b286d6ac430afec0d461163312c5841e", size = 11280411, upload-time = "2025-07-07T19:18:57.045Z" }, + { url = "https://files.pythonhosted.org/packages/da/01/e383018feba0a1ead6cf5fe8728e5d767fee02f06a3d800e82c489e5daaf/pandas-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dcb79bf373a47d2a40cf7232928eb7540155abbc460925c2c96d2d30b006eb4", size = 11988013, upload-time = "2025-07-07T19:18:59.771Z" }, + { url = "https://files.pythonhosted.org/packages/5b/14/cec7760d7c9507f11c97d64f29022e12a6cc4fc03ac694535e89f88ad2ec/pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8", size = 12767210, upload-time = "2025-07-07T19:19:02.944Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/6e2d2c6728ed29fb3d4d4d302504fb66f1a543e37eb2e43f352a86365cdf/pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679", size = 13440571, upload-time = "2025-07-07T19:19:06.82Z" }, + { url = "https://files.pythonhosted.org/packages/80/a5/3a92893e7399a691bad7664d977cb5e7c81cf666c81f89ea76ba2bff483d/pandas-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8", size = 10987601, upload-time = "2025-07-07T19:19:09.589Z" }, { url = "https://files.pythonhosted.org/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393, upload-time = "2025-07-07T19:19:12.245Z" }, { url = "https://files.pythonhosted.org/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750, upload-time = "2025-07-07T19:19:14.612Z" }, { url = "https://files.pythonhosted.org/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004, upload-time = "2025-07-07T19:19:16.857Z" }, @@ -5467,28 +5681,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] -[[package]] -name = "peft" -version = "0.18.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "accelerate" }, - { name = "huggingface-hub" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" } }, - { name = "packaging" }, - { name = "psutil" }, - { name = "pyyaml" }, - { name = "safetensors" }, - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "tqdm" }, - { name = "transformers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d8/48/147b3ea999560b40a34fd78724c7777aa9d18409c2250bdcaf9c4f2db7fc/peft-0.18.1.tar.gz", hash = "sha256:2dd0d6bfce936d1850e48aaddbd250941c5c02fc8ef3237cd8fd5aac35e0bae2", size = 635030, upload-time = "2026-01-09T13:08:01.136Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/14/b4e3f574acf349ae6f61f9c000a77f97a3b315b4bb6ad03791e79ae4a568/peft-0.18.1-py3-none-any.whl", hash = "sha256:0bf06847a3551e3019fc58c440cffc9a6b73e6e2962c95b52e224f77bbdb50f1", size = 556960, upload-time = "2026-01-09T13:07:55.865Z" }, -] - [[package]] name = "pexpect" version = "4.9.0" @@ -5503,60 +5695,68 @@ wheels = [ [[package]] name = "pillow" -version = "12.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, - { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, - { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, - { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, - { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, - { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, - { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, - { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, - { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, - { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, - { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, - { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, - { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, - { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, - { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, - { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, - { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, - { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, - { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, - { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, - { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, - { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, - { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, - { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, - { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, - { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, - { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, - { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, - { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, - { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, ] [[package]] @@ -5676,6 +5876,22 @@ version = "0.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload-time = "2025-06-09T22:54:30.551Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload-time = "2025-06-09T22:54:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload-time = "2025-06-09T22:54:33.929Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" }, + { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload-time = "2025-06-09T22:54:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" }, + { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload-time = "2025-06-09T22:54:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" }, + { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload-time = "2025-06-09T22:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload-time = "2025-06-09T22:54:53.234Z" }, { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, @@ -5810,6 +6026,13 @@ version = "21.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload-time = "2025-07-18T00:57:31.761Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/d4/d4f817b21aacc30195cf6a46ba041dd1be827efa4a623cc8bf39a1c2a0c0/pyarrow-21.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd", size = 31160305, upload-time = "2025-07-18T00:55:35.373Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9c/dcd38ce6e4b4d9a19e1d36914cb8e2b1da4e6003dd075474c4cfcdfe0601/pyarrow-21.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876", size = 32684264, upload-time = "2025-07-18T00:55:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/2a2d9f8d7a59b639523454bec12dba35ae3d0a07d8ab529dc0809f74b23c/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d", size = 41108099, upload-time = "2025-07-18T00:55:42.889Z" }, + { url = "https://files.pythonhosted.org/packages/ad/90/2660332eeb31303c13b653ea566a9918484b6e4d6b9d2d46879a33ab0622/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e", size = 42829529, upload-time = "2025-07-18T00:55:47.069Z" }, + { url = "https://files.pythonhosted.org/packages/33/27/1a93a25c92717f6aa0fca06eb4700860577d016cd3ae51aad0e0488ac899/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82", size = 43367883, upload-time = "2025-07-18T00:55:53.069Z" }, + { url = "https://files.pythonhosted.org/packages/05/d9/4d09d919f35d599bc05c6950095e358c3e15148ead26292dfca1fb659b0c/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623", size = 45133802, upload-time = "2025-07-18T00:55:57.714Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/f3795b6e192c3ab881325ffe172e526499eb3780e306a15103a2764916a2/pyarrow-21.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18", size = 26203175, upload-time = "2025-07-18T00:56:01.364Z" }, { url = "https://files.pythonhosted.org/packages/16/ca/c7eaa8e62db8fb37ce942b1ea0c6d7abfe3786ca193957afa25e71b81b66/pyarrow-21.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a", size = 31154306, upload-time = "2025-07-18T00:56:04.42Z" }, { url = "https://files.pythonhosted.org/packages/ce/e8/e87d9e3b2489302b3a1aea709aaca4b781c5252fcb812a17ab6275a9a484/pyarrow-21.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe", size = 32680622, upload-time = "2025-07-18T00:56:07.505Z" }, { url = "https://files.pythonhosted.org/packages/84/52/79095d73a742aa0aba370c7942b1b655f598069489ab387fe47261a849e1/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd", size = 41104094, upload-time = "2025-07-18T00:56:10.994Z" }, @@ -5828,11 +6051,11 @@ wheels = [ [[package]] name = "pyasn1" -version = "0.6.3" +version = "0.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, ] [[package]] @@ -5853,6 +6076,25 @@ version = "1.4.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/04/14/43297a7b7f0c1bf0c00b596f754ee3ac946128c64d21047ccf9c9bbc5165/pybase64-1.4.2.tar.gz", hash = "sha256:46cdefd283ed9643315d952fe44de80dc9b9a811ce6e3ec97fd1827af97692d0", size = 137246, upload-time = "2025-07-27T13:08:57.808Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/28/6d/11ede991e800797b9f5ebd528013b34eee5652df93de61ffb24503393fa5/pybase64-1.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2c75d1388855b5a1015b65096d7dbcc708e7de3245dcbedeb872ec05a09326", size = 38326, upload-time = "2025-07-27T13:03:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/fe/84/87f1f565f42e2397e2aaa2477c86419f5173c3699881c42325c090982f0a/pybase64-1.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b621a972a01841368fdb9dedc55fd3c6e0c7217d0505ba3b1ebe95e7ef1b493", size = 31661, upload-time = "2025-07-27T13:03:10.295Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2a/a24c810e7a61d2cc6f73fe9ee4872a03030887fa8654150901b15f376f65/pybase64-1.4.2-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f48c32ac6a16cbf57a5a96a073fef6ff7e3526f623cd49faa112b7f9980bafba", size = 68192, upload-time = "2025-07-27T13:03:11.467Z" }, + { url = "https://files.pythonhosted.org/packages/ee/87/d9baf98cbfc37b8657290ad4421f3a3c36aa0eafe4872c5859cfb52f3448/pybase64-1.4.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ace8b23093a6bb862477080d9059b784096ab2f97541e8bfc40d42f062875149", size = 71587, upload-time = "2025-07-27T13:03:12.719Z" }, + { url = "https://files.pythonhosted.org/packages/0b/89/3df043cc56ef3b91b7aa0c26ae822a2d7ec8da0b0fd7c309c879b0eb5988/pybase64-1.4.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1772c7532a7fb6301baea3dd3e010148dbf70cd1136a83c2f5f91bdc94822145", size = 59910, upload-time = "2025-07-27T13:03:14.266Z" }, + { url = "https://files.pythonhosted.org/packages/75/4f/6641e9edf37aeb4d4524dc7ba2168eff8d96c90e77f6283c2be3400ab380/pybase64-1.4.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:f86f7faddcba5cbfea475f8ab96567834c28bf09ca6c7c3d66ee445adac80d8f", size = 56701, upload-time = "2025-07-27T13:03:15.6Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7f/20d8ac1046f12420a0954a45a13033e75f98aade36eecd00c64e3549b071/pybase64-1.4.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0b8c8e275b5294089f314814b4a50174ab90af79d6a4850f6ae11261ff6a7372", size = 59288, upload-time = "2025-07-27T13:03:16.823Z" }, + { url = "https://files.pythonhosted.org/packages/17/ea/9c0ca570e3e50b3c6c3442e280c83b321a0464c86a9db1f982a4ff531550/pybase64-1.4.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:864d85a0470c615807ae8b97d724d068b940a2d10ac13a5f1b9e75a3ce441758", size = 60267, upload-time = "2025-07-27T13:03:18.132Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46894929d71ccedebbfb0284173b0fea96bc029cd262654ba8451a7035d6/pybase64-1.4.2-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:47254d97ed2d8351e30ecfdb9e2414547f66ba73f8a09f932c9378ff75cd10c5", size = 54801, upload-time = "2025-07-27T13:03:19.669Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/02c95218ea964f0b2469717c2c69b48e63f4ca9f18af01a5b2a29e4c1216/pybase64-1.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:264b65ecc4f0ee73f3298ab83bbd8008f7f9578361b8df5b448f985d8c63e02a", size = 58599, upload-time = "2025-07-27T13:03:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/15/45/ccc21004930789b8fb439d43e3212a6c260ccddb2bf450c39a20db093f33/pybase64-1.4.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fbcc2b30cd740c16c9699f596f22c7a9e643591311ae72b1e776f2d539e9dd9d", size = 52388, upload-time = "2025-07-27T13:03:23.064Z" }, + { url = "https://files.pythonhosted.org/packages/c4/45/22e46e549710c4c237d77785b6fb1bc4c44c288a5c44237ba9daf5c34b82/pybase64-1.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cda9f79c22d51ee4508f5a43b673565f1d26af4330c99f114e37e3186fdd3607", size = 68802, upload-time = "2025-07-27T13:03:24.673Z" }, + { url = "https://files.pythonhosted.org/packages/55/0c/232c6261b81296e5593549b36e6e7884a5da008776d12665923446322c36/pybase64-1.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0c91c6d2a7232e2a1cd10b3b75a8bb657defacd4295a1e5e80455df2dfc84d4f", size = 57841, upload-time = "2025-07-27T13:03:25.948Z" }, + { url = "https://files.pythonhosted.org/packages/20/8a/b35a615ae6f04550d696bb179c414538b3b477999435fdd4ad75b76139e4/pybase64-1.4.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:a370dea7b1cee2a36a4d5445d4e09cc243816c5bc8def61f602db5a6f5438e52", size = 54320, upload-time = "2025-07-27T13:03:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a9/8bd4f9bcc53689f1b457ecefed1eaa080e4949d65a62c31a38b7253d5226/pybase64-1.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9aa4de83f02e462a6f4e066811c71d6af31b52d7484de635582d0e3ec3d6cc3e", size = 56482, upload-time = "2025-07-27T13:03:28.942Z" }, + { url = "https://files.pythonhosted.org/packages/75/e5/4a7735b54a1191f61c3f5c2952212c85c2d6b06eb5fb3671c7603395f70c/pybase64-1.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83a1c2f9ed00fee8f064d548c8654a480741131f280e5750bb32475b7ec8ee38", size = 70959, upload-time = "2025-07-27T13:03:30.171Z" }, + { url = "https://files.pythonhosted.org/packages/d3/67/e2b6cb32c782e12304d467418e70da0212567f42bd4d3b5eb1fdf64920ad/pybase64-1.4.2-cp312-cp312-win32.whl", hash = "sha256:a6e5688b18d558e8c6b8701cc8560836c4bbeba61d33c836b4dba56b19423716", size = 33683, upload-time = "2025-07-27T13:03:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/4f/bc/d5c277496063a09707486180f17abbdbdebbf2f5c4441b20b11d3cb7dc7c/pybase64-1.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:c995d21b8bd08aa179cd7dd4db0695c185486ecc72da1e8f6c37ec86cadb8182", size = 35817, upload-time = "2025-07-27T13:03:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/e6/69/e4be18ae685acff0ae77f75d4586590f29d2cd187bf603290cf1d635cad4/pybase64-1.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:e254b9258c40509c2ea063a7784f6994988f3f26099d6e08704e3c15dfed9a55", size = 30900, upload-time = "2025-07-27T13:03:34.499Z" }, { url = "https://files.pythonhosted.org/packages/f4/56/5337f27a8b8d2d6693f46f7b36bae47895e5820bfa259b0072574a4e1057/pybase64-1.4.2-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:0f331aa59549de21f690b6ccc79360ffed1155c3cfbc852eb5c097c0b8565a2b", size = 33888, upload-time = "2025-07-27T13:03:35.698Z" }, { url = "https://files.pythonhosted.org/packages/4c/09/f3f4b11fc9beda7e8625e29fb0f549958fcbb34fea3914e1c1d95116e344/pybase64-1.4.2-cp313-cp313-android_21_x86_64.whl", hash = "sha256:9dad20bf1f3ed9e6fe566c4c9d07d9a6c04f5a280daebd2082ffb8620b0a880d", size = 40796, upload-time = "2025-07-27T13:03:36.927Z" }, { url = "https://files.pythonhosted.org/packages/e3/ff/470768f0fe6de0aa302a8cb1bdf2f9f5cffc3f69e60466153be68bc953aa/pybase64-1.4.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:69d3f0445b0faeef7bb7f93bf8c18d850785e2a77f12835f49e524cc54af04e7", size = 30914, upload-time = "2025-07-27T13:03:38.475Z" }, @@ -6025,6 +6267,20 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, @@ -6067,6 +6323,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, ] [[package]] @@ -6120,15 +6380,12 @@ wheels = [ ] [[package]] -name = "pydot" -version = "4.0.1" +name = "pydub" +version = "0.25.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyparsing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/35/b17cb89ff865484c6a20ef46bf9d95a5f07328292578de0b295f4a6beec2/pydot-4.0.1.tar.gz", hash = "sha256:c2148f681c4a33e08bf0e26a9e5f8e4099a82e0e2a068098f32ce86577364ad5", size = 162594, upload-time = "2025-06-17T20:09:56.454Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl", hash = "sha256:869c0efadd2708c0be1f916eb669f3d664ca684bc57ffb7ecc08e70d5e93fee6", size = 37087, upload-time = "2025-06-17T20:09:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" }, ] [[package]] @@ -6147,20 +6404,20 @@ wheels = [ [[package]] name = "pygments" -version = "2.20.0" +version = "2.19.2" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pyjwt" -version = "2.12.1" +version = "2.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, ] [package.optional-dependencies] @@ -6216,7 +6473,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.3" +version = "8.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, @@ -6225,21 +6482,21 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] [[package]] name = "pytest-asyncio" -version = "1.3.0" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, + { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" }, ] [[package]] @@ -6258,15 +6515,15 @@ wheels = [ [[package]] name = "pytest-testmon" -version = "2.2.0" +version = "2.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/1d/3e4230cc67cd6205bbe03c3527500c0ccaf7f0c78b436537eac71590ee4a/pytest_testmon-2.2.0.tar.gz", hash = "sha256:01f488e955ed0e0049777bee598bf1f647dd524e06f544c31a24e68f8d775a51", size = 23108, upload-time = "2025-12-01T07:30:24.76Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/24/b17712bc8b9d9814a30346e5bd76a6c4539f5187455f4e0d99d95f033da6/pytest_testmon-2.1.3.tar.gz", hash = "sha256:dad41aa7d501d74571750da1abd3f6673b63fd9dbf3023bd1623814999018c97", size = 22608, upload-time = "2024-12-22T12:43:28.822Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/55/ebb3c2f59fb089f08d00f764830d35780fc4e4c41dffcadafa3264682b65/pytest_testmon-2.2.0-py3-none-any.whl", hash = "sha256:2604ca44a54d61a2e830d9ce828b41a837075e4ebc1f81b148add8e90d34815b", size = 25199, upload-time = "2025-12-01T07:30:23.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/08/278800711d937e76ce59105fea1bb739ae5ff5c13583fd064fe3b4e64fa1/pytest_testmon-2.1.3-py3-none-any.whl", hash = "sha256:53ba06d8a90ce24c3a191b196aac72ca4b788beff5eb1c1bffee04dc50ec7105", size = 24994, upload-time = "2024-12-22T12:43:10.173Z" }, ] [[package]] @@ -6294,15 +6551,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] -[[package]] -name = "python-box" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/85/b02b80d74bdb95bfe491d49ad1627e9833c73d331edbe6eed0bdfe170361/python-box-6.1.0.tar.gz", hash = "sha256:6e7c243b356cb36e2c0f0e5ed7850969fede6aa812a7f501de7768996c7744d7", size = 41443, upload-time = "2022-10-29T22:30:45.515Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/c6/6d1e368710cb6c458ed692d179d7e101ebce80a3e640b2e74cc7ae886d6f/python_box-6.1.0-py3-none-any.whl", hash = "sha256:bdec0a5f5a17b01fc538d292602a077aa8c641fb121e1900dff0591791af80e8", size = 27277, upload-time = "2022-10-29T22:30:43.645Z" }, -] - [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -6356,6 +6604,9 @@ name = "pywin32" version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, @@ -6370,6 +6621,15 @@ version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, @@ -6426,18 +6686,52 @@ wheels = [ [[package]] name = "quack-kernels" -version = "0.3.9" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version < '3.13' and sys_platform == 'linux'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version < '3.13' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "apache-tvm-ffi" }, + { name = "nvidia-cutlass-dsl" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch-c-dlpack-ext" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/a9/f474f3a45193784b2a17f3403554855da9787824c620db8b4eb992d49297/quack_kernels-0.2.4.tar.gz", hash = "sha256:719ea9584af3e55bc1d4da00374ffea68ba4342fbaa6db7e380801c21d7df59a", size = 149411, upload-time = "2025-12-31T23:14:08.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/11/2bc6e7b4dd839aff42b8e22207b13d1253078640fad7392f2bb7d2d6b415/quack_kernels-0.2.4-py3-none-any.whl", hash = "sha256:db312c72abd1eae10c5ccfe6a794273025c9988c4f400fa55197dbdde8ac3835", size = 153664, upload-time = "2025-12-31T23:14:07.331Z" }, +] + +[[package]] +name = "quack-kernels" +version = "0.2.10" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version < '3.13' and sys_platform == 'darwin'", +] dependencies = [ { name = "apache-tvm-ffi" }, { name = "nvidia-cutlass-dsl" }, - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "torch-c-dlpack-ext" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/db/d2e480fd71c38b88ffcbf40298d604400c64e0ffcaa06d6aa61a87b2673a/quack_kernels-0.3.9.tar.gz", hash = "sha256:4fd272f52142e408a591b94be7c6a0261e222e034e599bce6da827eeae8ad04d", size = 212760, upload-time = "2026-04-05T06:34:58.642Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/28/231c862500fec531080cc733e5766b46518edaefe0a068d46b276c380a25/quack_kernels-0.2.10.tar.gz", hash = "sha256:df86e981ea76542467ae2cd9ac606d587658e8d648a51c34dc0f2913a3e26bf6", size = 161102, upload-time = "2026-02-18T22:20:50.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/a8/eea5885361143c19505a8e86890a681c363ac0f9ac6ba02b5c2c82ebe44b/quack_kernels-0.3.9-py3-none-any.whl", hash = "sha256:160364a32fd72df6e934adb2bb2ae324843ddccffc88aaa6f5de4c9a00ec7ac8", size = 216038, upload-time = "2026-04-05T06:34:57.426Z" }, + { url = "https://files.pythonhosted.org/packages/10/ac/8f70ddd397aff1d606d7aa6fbe857a2bc58a817965099cd97d91264175a8/quack_kernels-0.2.10-py3-none-any.whl", hash = "sha256:a5b604c5cf28d9e601aae00488b6b603bb4060ccab8409a4443e72a649226f74", size = 165298, upload-time = "2026-02-18T22:20:48.978Z" }, ] [[package]] @@ -6490,6 +6784,10 @@ dependencies = [ { name = "requests" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/16/45eefb51eb1767342a6dbf41af0b432279e422e56160705fcd1098a7ec53/ray-2.54.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:cf5c33b4b13850ec24a5bd5f9d9e0a8161f8e586bfd297e52913d170dec447fe", size = 70084880, upload-time = "2026-02-18T04:05:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/e07aca3637e9c3ec4857ec4366208099cf8488ece8061a9925ba29b66382/ray-2.54.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:795ae21d6b764245d3f521bc5833446d58569e7dfde9c5777417eb285d87450f", size = 72107346, upload-time = "2026-02-18T04:05:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/cc5ea8460c3dc602e6b7198277a7c59ba2b8929374ab22efa8df9f3deac8/ray-2.54.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:a972afd5aa3dda99d0b2f369b5f62e5dd95865ab7d37bf2e0a0e0d2cfbd9b325", size = 72967230, upload-time = "2026-02-18T04:05:33.771Z" }, + { url = "https://files.pythonhosted.org/packages/de/d7/744de3b1bb881701330ddcbb2f6efaccd65915d564ece899a3838f9fb105/ray-2.54.0-cp312-cp312-win_amd64.whl", hash = "sha256:2ee074ede491d0aacfa339c003f5d7a15826e1e2a72ce873234ccbc0446e19b3", size = 27427353, upload-time = "2026-02-18T04:05:38.853Z" }, { url = "https://files.pythonhosted.org/packages/7f/f2/5c0161d10445e703b7d01413ab54ec1cc5e27032555279d296df89b9c4ee/ray-2.54.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5ad77961fea16c697a0fb0e51216dd39c0bec28868cde54ac668edd58d12b8ae", size = 70030991, upload-time = "2026-02-18T04:05:43.966Z" }, { url = "https://files.pythonhosted.org/packages/fd/8c/4a4a38eaec6e9614076a96967f58540f4f8d4aa0c793f43150c5df23cb9a/ray-2.54.0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:8952c23a8aa94f10728c2d16e0dc3732d09aa0e6254801757ff494984a214f45", size = 72013826, upload-time = "2026-02-18T04:05:49.866Z" }, { url = "https://files.pythonhosted.org/packages/42/ac/e7ec2a406bd755f61c7090460fa5ab3f09b00c3c2d8db6d0b559f78a30eb/ray-2.54.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:ab89e6089abb6e46fb98fdd96d399b31a852d79127cd8ac00746c61d93defa2c", size = 72880209, upload-time = "2026-02-18T04:05:55.498Z" }, @@ -6523,6 +6821,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } wheels = [ @@ -6535,6 +6834,20 @@ version = "2025.7.34" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0b/de/e13fa6dc61d78b30ba47481f99933a3b49a57779d625c392d8036770a60d/regex-2025.7.34.tar.gz", hash = "sha256:9ead9765217afd04a86822dfcd4ed2747dfe426e887da413b15ff0ac2457e21a", size = 400714, upload-time = "2025-07-31T00:21:16.262Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/f0/31d62596c75a33f979317658e8d261574785c6cd8672c06741ce2e2e2070/regex-2025.7.34-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7f7211a746aced993bef487de69307a38c5ddd79257d7be83f7b202cb59ddb50", size = 485492, upload-time = "2025-07-31T00:19:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/d8/16/b818d223f1c9758c3434be89aa1a01aae798e0e0df36c1f143d1963dd1ee/regex-2025.7.34-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fb31080f2bd0681484b275461b202b5ad182f52c9ec606052020fe13eb13a72f", size = 290000, upload-time = "2025-07-31T00:19:37.175Z" }, + { url = "https://files.pythonhosted.org/packages/cd/70/69506d53397b4bd6954061bae75677ad34deb7f6ca3ba199660d6f728ff5/regex-2025.7.34-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0200a5150c4cf61e407038f4b4d5cdad13e86345dac29ff9dab3d75d905cf130", size = 286072, upload-time = "2025-07-31T00:19:38.612Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/536a216d5f66084fb577bb0543b5cb7de3272eb70a157f0c3a542f1c2551/regex-2025.7.34-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:739a74970e736df0773788377969c9fea3876c2fc13d0563f98e5503e5185f46", size = 797341, upload-time = "2025-07-31T00:19:40.119Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/733f8168449e56e8f404bb807ea7189f59507cbea1b67a7bbcd92f8bf844/regex-2025.7.34-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4fef81b2f7ea6a2029161ed6dea9ae13834c28eb5a95b8771828194a026621e4", size = 862556, upload-time = "2025-07-31T00:19:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/19/dd/59c464d58c06c4f7d87de4ab1f590e430821345a40c5d345d449a636d15f/regex-2025.7.34-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea74cf81fe61a7e9d77989050d0089a927ab758c29dac4e8e1b6c06fccf3ebf0", size = 910762, upload-time = "2025-07-31T00:19:43Z" }, + { url = "https://files.pythonhosted.org/packages/37/a8/b05ccf33ceca0815a1e253693b2c86544932ebcc0049c16b0fbdf18b688b/regex-2025.7.34-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4636a7f3b65a5f340ed9ddf53585c42e3ff37101d383ed321bfe5660481744b", size = 801892, upload-time = "2025-07-31T00:19:44.645Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9a/b993cb2e634cc22810afd1652dba0cae156c40d4864285ff486c73cd1996/regex-2025.7.34-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cef962d7834437fe8d3da6f9bfc6f93f20f218266dcefec0560ed7765f5fe01", size = 786551, upload-time = "2025-07-31T00:19:46.127Z" }, + { url = "https://files.pythonhosted.org/packages/2d/79/7849d67910a0de4e26834b5bb816e028e35473f3d7ae563552ea04f58ca2/regex-2025.7.34-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cbe1698e5b80298dbce8df4d8d1182279fbdaf1044e864cbc9d53c20e4a2be77", size = 856457, upload-time = "2025-07-31T00:19:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/91/c6/de516bc082524b27e45cb4f54e28bd800c01efb26d15646a65b87b13a91e/regex-2025.7.34-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:32b9f9bcf0f605eb094b08e8da72e44badabb63dde6b83bd530580b488d1c6da", size = 848902, upload-time = "2025-07-31T00:19:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/22/519ff8ba15f732db099b126f039586bd372da6cd4efb810d5d66a5daeda1/regex-2025.7.34-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:524c868ba527eab4e8744a9287809579f54ae8c62fbf07d62aacd89f6026b282", size = 788038, upload-time = "2025-07-31T00:19:50.794Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7d/aabb467d8f57d8149895d133c88eb809a1a6a0fe262c1d508eb9dfabb6f9/regex-2025.7.34-cp312-cp312-win32.whl", hash = "sha256:d600e58ee6d036081c89696d2bdd55d507498a7180df2e19945c6642fac59588", size = 264417, upload-time = "2025-07-31T00:19:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/3b/39/bd922b55a4fc5ad5c13753274e5b536f5b06ec8eb9747675668491c7ab7a/regex-2025.7.34-cp312-cp312-win_amd64.whl", hash = "sha256:9a9ab52a466a9b4b91564437b36417b76033e8778e5af8f36be835d8cb370d62", size = 275387, upload-time = "2025-07-31T00:19:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/c61d2fdcecb754a40475a3d1ef9a000911d3e3fc75c096acf44b0dfb786a/regex-2025.7.34-cp312-cp312-win_arm64.whl", hash = "sha256:c83aec91af9c6fbf7c743274fd952272403ad9a9db05fe9bfc9df8d12b45f176", size = 268482, upload-time = "2025-07-31T00:19:55.183Z" }, { url = "https://files.pythonhosted.org/packages/15/16/b709b2119975035169a25aa8e4940ca177b1a2e25e14f8d996d09130368e/regex-2025.7.34-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3c9740a77aeef3f5e3aaab92403946a8d34437db930a0280e7e81ddcada61f5", size = 485334, upload-time = "2025-07-31T00:19:56.58Z" }, { url = "https://files.pythonhosted.org/packages/94/a6/c09136046be0595f0331bc58a0e5f89c2d324cf734e0b0ec53cf4b12a636/regex-2025.7.34-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:69ed3bc611540f2ea70a4080f853741ec698be556b1df404599f8724690edbcd", size = 289942, upload-time = "2025-07-31T00:19:57.943Z" }, { url = "https://files.pythonhosted.org/packages/36/91/08fc0fd0f40bdfb0e0df4134ee37cfb16e66a1044ac56d36911fd01c69d2/regex-2025.7.34-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d03c6f9dcd562c56527c42b8530aad93193e0b3254a588be1f2ed378cdfdea1b", size = 285991, upload-time = "2025-07-31T00:19:59.837Z" }, @@ -6593,15 +6906,16 @@ wheels = [ ] [[package]] -name = "requests-toolbelt" -version = "1.0.0" +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/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +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/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, + { 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]] @@ -6637,6 +6951,20 @@ version = "0.6.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/73/46/05a94dc55ac03cf931d18e43b86ecee5ee054cb88b7853fffd741e35009c/rignore-0.6.4.tar.gz", hash = "sha256:e893fdd2d7fdcfa9407d0b7600ef2c2e2df97f55e1c45d4a8f54364829ddb0ab", size = 11633, upload-time = "2025-07-19T19:24:46.219Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6c/e5af4383cdd7829ef9aa63ac82a6507983e02dbc7c2e7b9aa64b7b8e2c7a/rignore-0.6.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:74720d074b79f32449d5d212ce732e0144a294a184246d1f1e7bcc1fc5c83b69", size = 885885, upload-time = "2025-07-19T19:23:53.236Z" }, + { url = "https://files.pythonhosted.org/packages/89/3e/1b02a868830e464769aa417ee195ac352fe71ff818df8ce50c4b998edb9c/rignore-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a8184fcf567bd6b6d7b85a0c138d98dd40f63054141c96b175844414c5530d7", size = 819736, upload-time = "2025-07-19T19:23:46.565Z" }, + { url = "https://files.pythonhosted.org/packages/e0/75/b9be0c523d97c09f3c6508a67ce376aba4efe41c333c58903a0d7366439a/rignore-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcb0d7d7ecc3fbccf6477bb187c04a091579ea139f15f139abe0b3b48bdfef69", size = 892779, upload-time = "2025-07-19T19:22:35.167Z" }, + { url = "https://files.pythonhosted.org/packages/91/f4/3064b06233697f2993485d132f06fe95061fef71631485da75aed246c4fd/rignore-0.6.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feac73377a156fb77b3df626c76f7e5893d9b4e9e886ac8c0f9d44f1206a2a91", size = 872116, upload-time = "2025-07-19T19:22:47.828Z" }, + { url = "https://files.pythonhosted.org/packages/99/94/cb8e7af9a3c0a665f10e2366144e0ebc66167cf846aca5f1ac31b3661598/rignore-0.6.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:465179bc30beb1f7a3439e428739a2b5777ed26660712b8c4e351b15a7c04483", size = 1163345, upload-time = "2025-07-19T19:23:00.557Z" }, + { url = "https://files.pythonhosted.org/packages/86/6b/49faa7ad85ceb6ccef265df40091d9992232d7f6055fa664fe0a8b13781c/rignore-0.6.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a4877b4dca9cf31a4d09845b300c677c86267657540d0b4d3e6d0ce3110e6e9", size = 939967, upload-time = "2025-07-19T19:23:13.494Z" }, + { url = "https://files.pythonhosted.org/packages/80/c8/b91afda10bd5ca1e3a80463340b899c0dc26a7750a9f3c94f668585c7f40/rignore-0.6.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:456456802b1e77d1e2d149320ee32505b8183e309e228129950b807d204ddd17", size = 949717, upload-time = "2025-07-19T19:23:36.404Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f1/88bfdde58ae3fb1c1a92bb801f492eea8eafcdaf05ab9b75130023a4670b/rignore-0.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c1ff2fc223f1d9473d36923160af37bf765548578eb9d47a2f52e90da8ae408", size = 975534, upload-time = "2025-07-19T19:23:25.988Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8f/a80b4a2e48ceba56ba19e096d41263d844757e10aa36ede212571b5d8117/rignore-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e445fbc214ae18e0e644a78086ea5d0f579e210229a4fbe86367d11a4cd03c11", size = 1067837, upload-time = "2025-07-19T19:23:59.888Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/0905597af0e78748909ef58418442a480ddd93e9fc89b0ca9ab170c357c0/rignore-0.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e07d9c5270fc869bc431aadcfb6ed0447f89b8aafaa666914c077435dc76a123", size = 1134959, upload-time = "2025-07-19T19:24:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/cc/7d/0fa29adf9183b61947ce6dc8a1a9779a8ea16573f557be28ec893f6ddbaa/rignore-0.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7a6ccc0ea83d2c0c6df6b166f2acacedcc220a516436490f41e99a5ae73b6019", size = 1109708, upload-time = "2025-07-19T19:24:24.176Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a7/92892ed86b2e36da403dd3a0187829f2d880414cef75bd612bfdf4dedebc/rignore-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:536392c5ec91755db48389546c833c4ab1426fe03e5a8522992b54ef8a244e7e", size = 1120546, upload-time = "2025-07-19T19:24:36.377Z" }, + { url = "https://files.pythonhosted.org/packages/31/1b/d29ae1fe901d523741d6d1d3ffe0d630734dd0ed6b047628a69c1e15ea44/rignore-0.6.4-cp312-cp312-win32.whl", hash = "sha256:f5f9dca46fc41c0a1e236767f68be9d63bdd2726db13a0ae3a30f68414472969", size = 642005, upload-time = "2025-07-19T19:24:56.671Z" }, + { url = "https://files.pythonhosted.org/packages/1a/41/a224944824688995374e4525115ce85fecd82442fc85edd5bcd81f4f256d/rignore-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:e02eecb9e1b9f9bf7c9030ae73308a777bed3b2486204cc74dfcfbe699ab1497", size = 720358, upload-time = "2025-07-19T19:24:49.959Z" }, { url = "https://files.pythonhosted.org/packages/db/a3/edd7d0d5cc0720de132b6651cef95ee080ce5fca11c77d8a47db848e5f90/rignore-0.6.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2b3b1e266ce45189240d14dfa1057f8013ea34b9bc8b3b44125ec8d25fdb3985", size = 885304, upload-time = "2025-07-19T19:23:54.268Z" }, { url = "https://files.pythonhosted.org/packages/93/a1/d8d2fb97a6548307507d049b7e93885d4a0dfa1c907af5983fd9f9362a21/rignore-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45fe803628cc14714df10e8d6cdc23950a47eb9eb37dfea9a4779f4c672d2aa0", size = 818799, upload-time = "2025-07-19T19:23:47.544Z" }, { url = "https://files.pythonhosted.org/packages/b1/cd/949981fcc180ad5ba7b31c52e78b74b2dea6b7bf744ad4c0c4b212f6da78/rignore-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e439f034277a947a4126e2da79dbb43e33d73d7c09d3d72a927e02f8a16f59aa", size = 892024, upload-time = "2025-07-19T19:22:36.18Z" }, @@ -6678,6 +7006,21 @@ version = "0.27.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1e/d9/991a0dee12d9fc53ed027e26a26a64b151d77252ac477e22666b9688bc16/rpds_py-0.27.0.tar.gz", hash = "sha256:8b23cf252f180cda89220b378d917180f29d313cd6a07b2431c0d3b776aae86f", size = 27420, upload-time = "2025-08-07T08:26:39.624Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/17/e67309ca1ac993fa1888a0d9b2f5ccc1f67196ace32e76c9f8e1dbbbd50c/rpds_py-0.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:19c990fdf5acecbf0623e906ae2e09ce1c58947197f9bced6bbd7482662231c4", size = 362611, upload-time = "2025-08-07T08:23:44.773Z" }, + { url = "https://files.pythonhosted.org/packages/93/2e/28c2fb84aa7aa5d75933d1862d0f7de6198ea22dfd9a0cca06e8a4e7509e/rpds_py-0.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c27a7054b5224710fcfb1a626ec3ff4f28bcb89b899148c72873b18210e446b", size = 347680, upload-time = "2025-08-07T08:23:46.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/3e/9834b4c8f4f5fe936b479e623832468aa4bd6beb8d014fecaee9eac6cdb1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09965b314091829b378b60607022048953e25f0b396c2b70e7c4c81bcecf932e", size = 384600, upload-time = "2025-08-07T08:23:48Z" }, + { url = "https://files.pythonhosted.org/packages/19/78/744123c7b38865a965cd9e6f691fde7ef989a00a256fa8bf15b75240d12f/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14f028eb47f59e9169bfdf9f7ceafd29dd64902141840633683d0bad5b04ff34", size = 400697, upload-time = "2025-08-07T08:23:49.407Z" }, + { url = "https://files.pythonhosted.org/packages/32/97/3c3d32fe7daee0a1f1a678b6d4dfb8c4dcf88197fa2441f9da7cb54a8466/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6168af0be75bba990a39f9431cdfae5f0ad501f4af32ae62e8856307200517b8", size = 517781, upload-time = "2025-08-07T08:23:50.557Z" }, + { url = "https://files.pythonhosted.org/packages/b2/be/28f0e3e733680aa13ecec1212fc0f585928a206292f14f89c0b8a684cad1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab47fe727c13c09d0e6f508e3a49e545008e23bf762a245b020391b621f5b726", size = 406449, upload-time = "2025-08-07T08:23:51.732Z" }, + { url = "https://files.pythonhosted.org/packages/95/ae/5d15c83e337c082d0367053baeb40bfba683f42459f6ebff63a2fd7e5518/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa01b3d5e3b7d97efab65bd3d88f164e289ec323a8c033c5c38e53ee25c007e", size = 386150, upload-time = "2025-08-07T08:23:52.822Z" }, + { url = "https://files.pythonhosted.org/packages/bf/65/944e95f95d5931112829e040912b25a77b2e7ed913ea5fe5746aa5c1ce75/rpds_py-0.27.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:6c135708e987f46053e0a1246a206f53717f9fadfba27174a9769ad4befba5c3", size = 406100, upload-time = "2025-08-07T08:23:54.339Z" }, + { url = "https://files.pythonhosted.org/packages/21/a4/1664b83fae02894533cd11dc0b9f91d673797c2185b7be0f7496107ed6c5/rpds_py-0.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc327f4497b7087d06204235199daf208fd01c82d80465dc5efa4ec9df1c5b4e", size = 421345, upload-time = "2025-08-07T08:23:55.832Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/b7303941c2b0823bfb34c71378249f8beedce57301f400acb04bb345d025/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e57906e38583a2cba67046a09c2637e23297618dc1f3caddbc493f2be97c93f", size = 561891, upload-time = "2025-08-07T08:23:56.951Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c8/48623d64d4a5a028fa99576c768a6159db49ab907230edddc0b8468b998b/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f4f69d7a4300fbf91efb1fb4916421bd57804c01ab938ab50ac9c4aa2212f03", size = 591756, upload-time = "2025-08-07T08:23:58.146Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/18f62617e8e61cc66334c9fb44b1ad7baae3438662098efbc55fb3fda453/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4c4fbbcff474e1e5f38be1bf04511c03d492d42eec0babda5d03af3b5589374", size = 557088, upload-time = "2025-08-07T08:23:59.6Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4c/e84c3a276e2496a93d245516be6b49e20499aa8ca1c94d59fada0d79addc/rpds_py-0.27.0-cp312-cp312-win32.whl", hash = "sha256:27bac29bbbf39601b2aab474daf99dbc8e7176ca3389237a23944b17f8913d97", size = 221926, upload-time = "2025-08-07T08:24:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/83/89/9d0fbcef64340db0605eb0a0044f258076f3ae0a3b108983b2c614d96212/rpds_py-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a06aa1197ec0281eb1d7daf6073e199eb832fe591ffa329b88bae28f25f5fe5", size = 233235, upload-time = "2025-08-07T08:24:01.846Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b0/e177aa9f39cbab060f96de4a09df77d494f0279604dc2f509263e21b05f9/rpds_py-0.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:e14aab02258cb776a108107bd15f5b5e4a1bbaa61ef33b36693dfab6f89d54f9", size = 223315, upload-time = "2025-08-07T08:24:03.337Z" }, { url = "https://files.pythonhosted.org/packages/81/d2/dfdfd42565a923b9e5a29f93501664f5b984a802967d48d49200ad71be36/rpds_py-0.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:443d239d02d9ae55b74015234f2cd8eb09e59fbba30bf60baeb3123ad4c6d5ff", size = 362133, upload-time = "2025-08-07T08:24:04.508Z" }, { url = "https://files.pythonhosted.org/packages/ac/4a/0a2e2460c4b66021d349ce9f6331df1d6c75d7eea90df9785d333a49df04/rpds_py-0.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8a7acf04fda1f30f1007f3cc96d29d8cf0a53e626e4e1655fdf4eabc082d367", size = 347128, upload-time = "2025-08-07T08:24:05.695Z" }, { url = "https://files.pythonhosted.org/packages/35/8d/7d1e4390dfe09d4213b3175a3f5a817514355cb3524593380733204f20b9/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0f92b78cfc3b74a42239fdd8c1266f4715b573204c234d2f9fc3fc7a24f185", size = 384027, upload-time = "2025-08-07T08:24:06.841Z" }, @@ -6801,6 +7144,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, ] +[[package]] +name = "safehttpx" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/d1/4282284d9cf1ee873607a46442da977fc3c985059315ab23610be31d5885/safehttpx-0.1.7.tar.gz", hash = "sha256:db201c0978c41eddb8bb480f3eee59dd67304fdd91646035e9d9a720049a9d23", size = 10385, upload-time = "2025-10-24T18:30:09.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/a3/0f0b7d78e2f1eb9e8e1afbff1d2bff8d60144aee17aca51c065b516743dd/safehttpx-0.1.7-py3-none-any.whl", hash = "sha256:c4f4a162db6993464d7ca3d7cc4af0ffc6515a606dfd220b9f82c6945d869cde", size = 8959, upload-time = "2025-10-24T18:30:08.733Z" }, +] + [[package]] name = "safetensors" version = "0.6.2" @@ -6842,13 +7197,18 @@ version = "1.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "joblib" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "scipy" }, { name = "threadpoolctl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/84/5f4af978fff619706b8961accac84780a6d298d82a8873446f72edb4ead0/scikit_learn-1.7.1.tar.gz", hash = "sha256:24b3f1e976a4665aa74ee0fcaac2b8fccc6ae77c8e07ab25da3ba6d3292b9802", size = 7190445, upload-time = "2025-07-18T08:01:54.5Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/16/57f176585b35ed865f51b04117947fe20f130f78940c6477b6d66279c9c2/scikit_learn-1.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3cee419b49b5bbae8796ecd690f97aa412ef1674410c23fc3257c6b8b85b8087", size = 9260431, upload-time = "2025-07-18T08:01:22.77Z" }, + { url = "https://files.pythonhosted.org/packages/67/4e/899317092f5efcab0e9bc929e3391341cec8fb0e816c4789686770024580/scikit_learn-1.7.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2fd8b8d35817b0d9ebf0b576f7d5ffbbabdb55536b0655a8aaae629d7ffd2e1f", size = 8637191, upload-time = "2025-07-18T08:01:24.731Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1b/998312db6d361ded1dd56b457ada371a8d8d77ca2195a7d18fd8a1736f21/scikit_learn-1.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:588410fa19a96a69763202f1d6b7b91d5d7a5d73be36e189bc6396bfb355bd87", size = 9486346, upload-time = "2025-07-18T08:01:26.713Z" }, + { url = "https://files.pythonhosted.org/packages/ad/09/a2aa0b4e644e5c4ede7006748f24e72863ba2ae71897fecfd832afea01b4/scikit_learn-1.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3142f0abe1ad1d1c31a2ae987621e41f6b578144a911ff4ac94781a583adad7", size = 9290988, upload-time = "2025-07-18T08:01:28.938Z" }, + { url = "https://files.pythonhosted.org/packages/15/fa/c61a787e35f05f17fc10523f567677ec4eeee5f95aa4798dbbbcd9625617/scikit_learn-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3ddd9092c1bd469acab337d87930067c87eac6bd544f8d5027430983f1e1ae88", size = 8735568, upload-time = "2025-07-18T08:01:30.936Z" }, { url = "https://files.pythonhosted.org/packages/52/f8/e0533303f318a0f37b88300d21f79b6ac067188d4824f1047a37214ab718/scikit_learn-1.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b7839687fa46d02e01035ad775982f2470be2668e13ddd151f0f55a5bf123bae", size = 9213143, upload-time = "2025-07-18T08:01:32.942Z" }, { url = "https://files.pythonhosted.org/packages/71/f3/f1df377d1bdfc3e3e2adc9c119c238b182293e6740df4cbeac6de2cc3e23/scikit_learn-1.7.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a10f276639195a96c86aa572ee0698ad64ee939a7b042060b98bd1930c261d10", size = 8591977, upload-time = "2025-07-18T08:01:34.967Z" }, { url = "https://files.pythonhosted.org/packages/99/72/c86a4cd867816350fe8dee13f30222340b9cd6b96173955819a5561810c5/scikit_learn-1.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:13679981fdaebc10cc4c13c43344416a86fcbc61449cb3e6517e1df9d12c8309", size = 9436142, upload-time = "2025-07-18T08:01:37.397Z" }, @@ -6866,11 +7226,20 @@ name = "scipy" version = "1.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/4a/b927028464795439faec8eaf0b03b011005c487bb2d07409f28bf30879c4/scipy-1.16.1.tar.gz", hash = "sha256:44c76f9e8b6e8e488a586190ab38016e4ed2f8a038af7cd3defa903c0a2238b3", size = 30580861, upload-time = "2025-07-27T16:33:30.834Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/d9/ec4864f5896232133f51382b54a08de91a9d1af7a76dfa372894026dfee2/scipy-1.16.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81b433bbeaf35728dad619afc002db9b189e45eebe2cd676effe1fb93fef2b9c", size = 36575194, upload-time = "2025-07-27T16:27:41.321Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6d/40e81ecfb688e9d25d34a847dca361982a6addf8e31f0957b1a54fbfa994/scipy-1.16.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:886cc81fdb4c6903a3bb0464047c25a6d1016fef77bb97949817d0c0d79f9e04", size = 28594590, upload-time = "2025-07-27T16:27:49.204Z" }, + { url = "https://files.pythonhosted.org/packages/0e/37/9f65178edfcc629377ce9a64fc09baebea18c80a9e57ae09a52edf84880b/scipy-1.16.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:15240c3aac087a522b4eaedb09f0ad061753c5eebf1ea430859e5bf8640d5919", size = 20866458, upload-time = "2025-07-27T16:27:54.98Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7b/749a66766871ea4cb1d1ea10f27004db63023074c22abed51f22f09770e0/scipy-1.16.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f81a25805f3659b48126b5053d9e823d3215e4a63730b5e1671852a1705921", size = 23539318, upload-time = "2025-07-27T16:28:01.604Z" }, + { url = "https://files.pythonhosted.org/packages/c4/db/8d4afec60eb833a666434d4541a3151eedbf2494ea6d4d468cbe877f00cd/scipy-1.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6c62eea7f607f122069b9bad3f99489ddca1a5173bef8a0c75555d7488b6f725", size = 33292899, upload-time = "2025-07-27T16:28:09.147Z" }, + { url = "https://files.pythonhosted.org/packages/51/1e/79023ca3bbb13a015d7d2757ecca3b81293c663694c35d6541b4dca53e98/scipy-1.16.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f965bbf3235b01c776115ab18f092a95aa74c271a52577bcb0563e85738fd618", size = 35162637, upload-time = "2025-07-27T16:28:17.535Z" }, + { url = "https://files.pythonhosted.org/packages/b6/49/0648665f9c29fdaca4c679182eb972935b3b4f5ace41d323c32352f29816/scipy-1.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f006e323874ffd0b0b816d8c6a8e7f9a73d55ab3b8c3f72b752b226d0e3ac83d", size = 35490507, upload-time = "2025-07-27T16:28:25.705Z" }, + { url = "https://files.pythonhosted.org/packages/62/8f/66cbb9d6bbb18d8c658f774904f42a92078707a7c71e5347e8bf2f52bb89/scipy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8fd15fc5085ab4cca74cb91fe0a4263b1f32e4420761ddae531ad60934c2119", size = 37923998, upload-time = "2025-07-27T16:28:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/14/c3/61f273ae550fbf1667675701112e380881905e28448c080b23b5a181df7c/scipy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7b8013c6c066609577d910d1a2a077021727af07b6fab0ee22c2f901f22352a", size = 38508060, upload-time = "2025-07-27T16:28:43.242Z" }, { url = "https://files.pythonhosted.org/packages/93/0b/b5c99382b839854a71ca9482c684e3472badc62620287cbbdab499b75ce6/scipy-1.16.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5451606823a5e73dfa621a89948096c6528e2896e40b39248295d3a0138d594f", size = 36533717, upload-time = "2025-07-27T16:28:51.706Z" }, { url = "https://files.pythonhosted.org/packages/eb/e5/69ab2771062c91e23e07c12e7d5033a6b9b80b0903ee709c3c36b3eb520c/scipy-1.16.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:89728678c5ca5abd610aee148c199ac1afb16e19844401ca97d43dc548a354eb", size = 28570009, upload-time = "2025-07-27T16:28:57.017Z" }, { url = "https://files.pythonhosted.org/packages/f4/69/bd75dbfdd3cf524f4d753484d723594aed62cfaac510123e91a6686d520b/scipy-1.16.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e756d688cb03fd07de0fffad475649b03cb89bee696c98ce508b17c11a03f95c", size = 20841942, upload-time = "2025-07-27T16:29:01.152Z" }, @@ -6924,6 +7293,14 @@ version = "0.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/be/32ce495aa1d0e0c323dcb1ba87096037358edee539cac5baf8755a6bd396/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57cae326c8727de58c85977b175af132a7138d84c764635d7e71bbee7e774133", size = 1943152, upload-time = "2025-08-12T06:59:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/88/7e/ff23008899a58678e98c6ff592bf4d368eee5a71af96d0df6b38a039dd4f/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56dd39a3c4d6493db3cdca7e8cc68c6b633f0d4195495cbadfcf5af8a22d05a6", size = 1325651, upload-time = "2025-08-12T06:59:41.536Z" }, + { url = "https://files.pythonhosted.org/packages/19/84/42eb3ce4796777a1b5d3699dfd4dca85113e68b637f194a6c8d786f16a04/sentencepiece-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9381351182ff9888cc80e41c632e7e274b106f450de33d67a9e8f6043da6f76", size = 1253645, upload-time = "2025-08-12T06:59:42.903Z" }, + { url = "https://files.pythonhosted.org/packages/89/fa/d3d5ebcba3cb9e6d3775a096251860c41a6bc53a1b9461151df83fe93255/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f955df238021bf11f0fc37cdb54fd5e5b5f7fd30ecc3d93fb48b6815437167", size = 1316273, upload-time = "2025-08-12T06:59:44.476Z" }, + { url = "https://files.pythonhosted.org/packages/04/88/14f2f4a2b922d8b39be45bf63d79e6cd3a9b2f248b2fcb98a69b12af12f5/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdfecef430d985f1c2bcbfff3defd1d95dae876fbd0173376012d2d7d24044b", size = 1387881, upload-time = "2025-08-12T06:59:46.09Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b8/903e5ccb77b4ef140605d5d71b4f9e0ad95d456d6184688073ed11712809/sentencepiece-0.2.1-cp312-cp312-win32.whl", hash = "sha256:a483fd29a34c3e34c39ac5556b0a90942bec253d260235729e50976f5dba1068", size = 999540, upload-time = "2025-08-12T06:59:48.023Z" }, + { url = "https://files.pythonhosted.org/packages/2d/81/92df5673c067148c2545b1bfe49adfd775bcc3a169a047f5a0e6575ddaca/sentencepiece-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4cdc7c36234fda305e85c32949c5211faaf8dd886096c7cea289ddc12a2d02de", size = 1054671, upload-time = "2025-08-12T06:59:49.895Z" }, + { url = "https://files.pythonhosted.org/packages/fe/02/c5e3bc518655d714622bec87d83db9cdba1cd0619a4a04e2109751c4f47f/sentencepiece-0.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:daeb5e9e9fcad012324807856113708614d534f596d5008638eb9b40112cd9e4", size = 1033923, upload-time = "2025-08-12T06:59:51.952Z" }, { url = "https://files.pythonhosted.org/packages/ba/4a/85fbe1706d4d04a7e826b53f327c4b80f849cf1c7b7c5e31a20a97d8f28b/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dcd8161eee7b41aae57ded06272905dbd680a0a04b91edd0f64790c796b2f706", size = 1943150, upload-time = "2025-08-12T06:59:53.588Z" }, { url = "https://files.pythonhosted.org/packages/c2/83/4cfb393e287509fc2155480b9d184706ef8d9fa8cbf5505d02a5792bf220/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c6c8f42949f419ff8c7e9960dbadcfbc982d7b5efc2f6748210d3dd53a7de062", size = 1325651, upload-time = "2025-08-12T06:59:55.073Z" }, { url = "https://files.pythonhosted.org/packages/8d/de/5a007fb53b1ab0aafc69d11a5a3dd72a289d5a3e78dcf2c3a3d9b14ffe93/sentencepiece-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:097f3394e99456e9e4efba1737c3749d7e23563dd1588ce71a3d007f25475fff", size = 1253641, upload-time = "2025-08-12T06:59:56.562Z" }, @@ -6977,6 +7354,16 @@ 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" }, { url = "https://files.pythonhosted.org/packages/5d/2f/fcedcade3b307a391b6e17c774c6261a7166aed641aee00ed2aad96c63ce/setproctitle-1.3.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3736b2a423146b5e62230502e47e08e68282ff3b69bcfe08a322bee73407922", size = 18047, upload-time = "2025-09-05T12:49:50.271Z" }, { url = "https://files.pythonhosted.org/packages/23/ae/afc141ca9631350d0a80b8f287aac79a76f26b6af28fd8bf92dae70dc2c5/setproctitle-1.3.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3384e682b158d569e85a51cfbde2afd1ab57ecf93ea6651fe198d0ba451196ee", size = 13073, upload-time = "2025-09-05T12:49:51.46Z" }, { url = "https://files.pythonhosted.org/packages/87/ed/0a4f00315bc02510395b95eec3d4aa77c07192ee79f0baae77ea7b9603d8/setproctitle-1.3.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0564a936ea687cd24dffcea35903e2a20962aa6ac20e61dd3a207652401492dd", size = 33284, upload-time = "2025-09-05T12:49:52.741Z" }, @@ -7028,10 +7415,21 @@ 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 = "sgl-kernel" +version = "0.3.21" +source = { git = "https://github.com/JustinTong0323/sglang.git?subdirectory=sgl-kernel&rev=70aa688742dd2b75bf9e8e980249303f39295b0d#70aa688742dd2b75bf9e8e980249303f39295b0d" } +dependencies = [ + { name = "scikit-build-core" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "wheel" }, +] + [[package]] name = "sglang" -version = "0.5.10" -source = { git = "https://github.com/sgl-project/sglang.git?subdirectory=python&tag=v0.5.10#1519acf37c23f2189adb93f57ca9cd2db1bebf18" } +version = "0.5.7.dev0" +source = { git = "https://github.com/JustinTong0323/sglang.git?subdirectory=python&rev=70aa688742dd2b75bf9e8e980249303f39295b0d#70aa688742dd2b75bf9e8e980249303f39295b0d" } dependencies = [ { name = "aiohttp" }, { name = "anthropic" }, @@ -7044,14 +7442,15 @@ dependencies = [ { name = "decord2" }, { name = "einops" }, { name = "fastapi" }, - { name = "flash-attn-4" }, { name = "flashinfer-cubin" }, { name = "flashinfer-python" }, { name = "gguf" }, + { name = "grpcio" }, + { name = "grpcio-health-checking" }, + { name = "grpcio-reflection" }, { name = "interegular" }, { name = "ipython" }, { name = "llguidance" }, - { name = "mistral-common" }, { name = "modelscope" }, { name = "msgspec" }, { name = "ninja" }, @@ -7072,13 +7471,13 @@ dependencies = [ { name = "pydantic" }, { name = "python-multipart" }, { name = "pyzmq" }, - { name = "quack-kernels" }, + { name = "quack-kernels", version = "0.2.4", source = { registry = "https://pypi.org/simple" } }, { name = "requests" }, { name = "scipy" }, { name = "sentencepiece" }, { name = "setproctitle" }, - { name = "sglang-kernel" }, - { name = "smg-grpc-servicer" }, + { name = "sgl-kernel" }, + { name = "smg-grpc-proto" }, { name = "soundfile" }, { name = "tiktoken" }, { name = "timm" }, @@ -7086,8 +7485,7 @@ dependencies = [ { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "torch-memory-saver" }, { name = "torchao", version = "0.9.0", source = { registry = "https://pypi.org/simple" } }, - { name = "torchaudio", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torchaudio", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torchaudio" }, { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l') or sys_platform != 'linux'" }, { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "torchvision", version = "0.25.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, @@ -7095,19 +7493,7 @@ dependencies = [ { name = "transformers" }, { name = "uvicorn" }, { name = "uvloop" }, - { name = "watchfiles" }, - { name = "xgrammar" }, -] - -[[package]] -name = "sglang-kernel" -version = "0.4.1" -source = { git = "https://github.com/sgl-project/sglang.git?subdirectory=sgl-kernel&tag=v0.5.10#1519acf37c23f2189adb93f57ca9cd2db1bebf18" } -dependencies = [ - { name = "scikit-build-core" }, - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "wheel" }, + { name = "xgrammar", version = "0.1.27", source = { registry = "https://pypi.org/simple" } }, ] [[package]] @@ -7125,6 +7511,19 @@ version = "3.20.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/af/92/51b417685abd96b31308b61b9acce7ec50d8e1de8fbc39a7fd4962c60689/simplejson-3.20.1.tar.gz", hash = "sha256:e64139b4ec4f1f24c142ff7dcafe55a22b811a74d86d66560c8815687143037d", size = 85591, upload-time = "2025-02-15T05:18:53.15Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/eb/34c16a1ac9ba265d024dc977ad84e1659d931c0a700967c3e59a98ed7514/simplejson-3.20.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f31c4a3a7ab18467ee73a27f3e59158255d1520f3aad74315edde7a940f1be23", size = 93100, upload-time = "2025-02-15T05:16:38.801Z" }, + { url = "https://files.pythonhosted.org/packages/41/fc/2c2c007d135894971e6814e7c0806936e5bade28f8db4dd7e2a58b50debd/simplejson-3.20.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:884e6183d16b725e113b83a6fc0230152ab6627d4d36cb05c89c2c5bccfa7bc6", size = 75464, upload-time = "2025-02-15T05:16:40.905Z" }, + { url = "https://files.pythonhosted.org/packages/0f/05/2b5ecb33b776c34bb5cace5de5d7669f9b60e3ca13c113037b2ca86edfbd/simplejson-3.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03d7a426e416fe0d3337115f04164cd9427eb4256e843a6b8751cacf70abc832", size = 75112, upload-time = "2025-02-15T05:16:42.246Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/1f3609a2792f06cd4b71030485f78e91eb09cfd57bebf3116bf2980a8bac/simplejson-3.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:000602141d0bddfcff60ea6a6e97d5e10c9db6b17fd2d6c66199fa481b6214bb", size = 150182, upload-time = "2025-02-15T05:16:43.557Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b0/053fbda38b8b602a77a4f7829def1b4f316cd8deb5440a6d3ee90790d2a4/simplejson-3.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af8377a8af78226e82e3a4349efdde59ffa421ae88be67e18cef915e4023a595", size = 158363, upload-time = "2025-02-15T05:16:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4b/2eb84ae867539a80822e92f9be4a7200dffba609275faf99b24141839110/simplejson-3.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15c7de4c88ab2fbcb8781a3b982ef883696736134e20b1210bca43fb42ff1acf", size = 148415, upload-time = "2025-02-15T05:16:47.861Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bd/400b0bd372a5666addf2540c7358bfc3841b9ce5cdbc5cc4ad2f61627ad8/simplejson-3.20.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:455a882ff3f97d810709f7b620007d4e0aca8da71d06fc5c18ba11daf1c4df49", size = 152213, upload-time = "2025-02-15T05:16:49.25Z" }, + { url = "https://files.pythonhosted.org/packages/50/12/143f447bf6a827ee9472693768dc1a5eb96154f8feb140a88ce6973a3cfa/simplejson-3.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fc0f523ce923e7f38eb67804bc80e0a028c76d7868500aa3f59225574b5d0453", size = 150048, upload-time = "2025-02-15T05:16:51.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ea/dd9b3e8e8ed710a66f24a22c16a907c9b539b6f5f45fd8586bd5c231444e/simplejson-3.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76461ec929282dde4a08061071a47281ad939d0202dc4e63cdd135844e162fbc", size = 151668, upload-time = "2025-02-15T05:16:53Z" }, + { url = "https://files.pythonhosted.org/packages/99/af/ee52a8045426a0c5b89d755a5a70cc821815ef3c333b56fbcad33c4435c0/simplejson-3.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ab19c2da8c043607bde4d4ef3a6b633e668a7d2e3d56f40a476a74c5ea71949f", size = 158840, upload-time = "2025-02-15T05:16:54.851Z" }, + { url = "https://files.pythonhosted.org/packages/68/db/ab32869acea6b5de7d75fa0dac07a112ded795d41eaa7e66c7813b17be95/simplejson-3.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2578bedaedf6294415197b267d4ef678fea336dd78ee2a6d2f4b028e9d07be3", size = 154212, upload-time = "2025-02-15T05:16:56.318Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/e3132d454977d75a3bf9a6d541d730f76462ebf42a96fea2621498166f41/simplejson-3.20.1-cp312-cp312-win32.whl", hash = "sha256:339f407373325a36b7fd744b688ba5bae0666b5d340ec6d98aebc3014bf3d8ea", size = 74101, upload-time = "2025-02-15T05:16:57.746Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5d/4e243e937fa3560107c69f6f7c2eed8589163f5ed14324e864871daa2dd9/simplejson-3.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:627d4486a1ea7edf1f66bb044ace1ce6b4c1698acd1b05353c97ba4864ea2e17", size = 75736, upload-time = "2025-02-15T05:16:59.017Z" }, { url = "https://files.pythonhosted.org/packages/c4/03/0f453a27877cb5a5fff16a975925f4119102cc8552f52536b9a98ef0431e/simplejson-3.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:71e849e7ceb2178344998cbe5ade101f1b329460243c79c27fbfc51c0447a7c3", size = 93109, upload-time = "2025-02-15T05:17:00.377Z" }, { url = "https://files.pythonhosted.org/packages/74/1f/a729f4026850cabeaff23e134646c3f455e86925d2533463420635ae54de/simplejson-3.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b63fdbab29dc3868d6f009a59797cefaba315fd43cd32ddd998ee1da28e50e29", size = 75475, upload-time = "2025-02-15T05:17:02.544Z" }, { url = "https://files.pythonhosted.org/packages/e2/14/50a2713fee8ff1f8d655b1a14f4a0f1c0c7246768a1b3b3d12964a4ed5aa/simplejson-3.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1190f9a3ce644fd50ec277ac4a98c0517f532cfebdcc4bd975c0979a9f05e1fb", size = 75112, upload-time = "2025-02-15T05:17:03.875Z" }, @@ -7150,23 +7549,6 @@ 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 = "skops" -version = "0.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, - { name = "packaging" }, - { name = "prettytable" }, - { name = "scikit-learn" }, - { name = "scipy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b5/0c/5ec987633e077dd0076178ea6ade2d6e57780b34afea0b497fb507d7a1ed/skops-0.13.0.tar.gz", hash = "sha256:66949fd3c95cbb5c80270fbe40293c0fe1e46cb4a921860e42584dd9c20ebeb1", size = 581312, upload-time = "2025-08-06T09:48:14.916Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/e8/6a2b2030f0689f894432b9c2f0357f2f3286b2a00474827e04b8fe9eea13/skops-0.13.0-py3-none-any.whl", hash = "sha256:55e2cccb18c86f5916e4cfe5acf55ed7b0eecddf08a151906414c092fa5926dc", size = 131200, upload-time = "2025-08-06T09:48:13.356Z" }, -] - [[package]] name = "smart-open" version = "7.3.0.post1" @@ -7181,30 +7563,15 @@ wheels = [ [[package]] name = "smg-grpc-proto" -version = "0.4.6" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/4b/a77cf46daf8da941d37f7653b7be41bc0c74ddd7a033e82dbab152aebc4f/smg_grpc_proto-0.4.6.tar.gz", hash = "sha256:3c8b2bf27efcf241fda166dffae8f0b986fcfc8e82836c12c86ed663827e3339", size = 16717, upload-time = "2026-04-09T16:34:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/e6/c2fceb12d8a954b7d0a8349822234bb8a00fc0fe7f72b53adfeacba83bb1/smg_grpc_proto-0.4.6-py3-none-any.whl", hash = "sha256:d79788ccadc53f446959da2fb2cfae58bb22d6b11ea03e2eb4b04ecffb03b97c", size = 57145, upload-time = "2026-04-09T16:34:24.788Z" }, -] - -[[package]] -name = "smg-grpc-servicer" -version = "0.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "grpcio" }, - { name = "grpcio-health-checking" }, - { name = "grpcio-reflection" }, - { name = "smg-grpc-proto" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8f/38/68c781d764800e11b9ede3533ea90df3c2b0ea0499016094417d5bc1ddb1/smg_grpc_servicer-0.5.2.tar.gz", hash = "sha256:dc8c809fdd1fe6be61289e2afad88f4f575642a6d7802660c5a91dfbd64a8971", size = 37079, upload-time = "2026-04-09T16:35:04.771Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/75/d1a07c0493d9e0a35c2bbb1e0f0ff5beaae4828475bbafd50882522f606a/smg_grpc_proto-0.4.2.tar.gz", hash = "sha256:be80b940b4a286896581ebd92a906d8369ebe14d363cf411ae250077dd6e3a80", size = 16610, upload-time = "2026-03-04T06:21:47.209Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/b9/e86889dd5f405f845b5dd991dd91b4dfb4ff566d98449c97c980ad6f83e2/smg_grpc_servicer-0.5.2-py3-none-any.whl", hash = "sha256:2a10c50ffa3615b1eebeefb5386b0be12d1a4dc4ca5ce05a13c94b7802f1d436", size = 40582, upload-time = "2026-04-09T16:35:03.822Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a2/5417b23d0a46cfd2531e20ead5efb73e6800433c5ae82afcdeea5b355be9/smg_grpc_proto-0.4.2-py3-none-any.whl", hash = "sha256:3ffaef3c15e8d03a2b075dad2510775421dce10683b3caf55710638bcc402dc7", size = 57216, upload-time = "2026-03-04T06:21:46.374Z" }, ] [[package]] @@ -7249,8 +7616,8 @@ version = "0.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" } wheels = [ @@ -7464,6 +7831,14 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/61/db/20c78f1081446095450bdc6ee6cc10045fce67a8e003a5876b6eaafc5cc4/sqlalchemy-2.0.43-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:20d81fc2736509d7a2bd33292e489b056cbae543661bb7de7ce9f1c0cd6e7f24", size = 2134891, upload-time = "2025-08-11T15:51:13.019Z" }, + { url = "https://files.pythonhosted.org/packages/45/0a/3d89034ae62b200b4396f0f95319f7d86e9945ee64d2343dcad857150fa2/sqlalchemy-2.0.43-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b9fc27650ff5a2c9d490c13c14906b918b0de1f8fcbb4c992712d8caf40e83", size = 2123061, upload-time = "2025-08-11T15:51:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/cb/10/2711f7ff1805919221ad5bee205971254845c069ee2e7036847103ca1e4c/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6772e3ca8a43a65a37c88e2f3e2adfd511b0b1da37ef11ed78dea16aeae85bd9", size = 3320384, upload-time = "2025-08-11T15:52:35.088Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0e/3d155e264d2ed2778484006ef04647bc63f55b3e2d12e6a4f787747b5900/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a113da919c25f7f641ffbd07fbc9077abd4b3b75097c888ab818f962707eb48", size = 3329648, upload-time = "2025-08-11T15:56:34.153Z" }, + { url = "https://files.pythonhosted.org/packages/5b/81/635100fb19725c931622c673900da5efb1595c96ff5b441e07e3dd61f2be/sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4286a1139f14b7d70141c67a8ae1582fc2b69105f1b09d9573494eb4bb4b2687", size = 3258030, upload-time = "2025-08-11T15:52:36.933Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ed/a99302716d62b4965fded12520c1cbb189f99b17a6d8cf77611d21442e47/sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:529064085be2f4d8a6e5fab12d36ad44f1909a18848fcfbdb59cc6d4bbe48efe", size = 3294469, upload-time = "2025-08-11T15:56:35.553Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a2/3a11b06715149bf3310b55a98b5c1e84a42cfb949a7b800bc75cb4e33abc/sqlalchemy-2.0.43-cp312-cp312-win32.whl", hash = "sha256:b535d35dea8bbb8195e7e2b40059e2253acb2b7579b73c1b432a35363694641d", size = 2098906, upload-time = "2025-08-11T15:55:00.645Z" }, + { url = "https://files.pythonhosted.org/packages/bc/09/405c915a974814b90aa591280623adc6ad6b322f61fd5cff80aeaef216c9/sqlalchemy-2.0.43-cp312-cp312-win_amd64.whl", hash = "sha256:1c6d85327ca688dbae7e2b06d7d84cfe4f3fffa5b5f9e21bb6ce9d0e1a0e0e0a", size = 2126260, upload-time = "2025-08-11T15:55:02.965Z" }, { url = "https://files.pythonhosted.org/packages/41/1c/a7260bd47a6fae7e03768bf66451437b36451143f36b285522b865987ced/sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", size = 2130598, upload-time = "2025-08-11T15:51:15.903Z" }, { url = "https://files.pythonhosted.org/packages/8e/84/8a337454e82388283830b3586ad7847aa9c76fdd4f1df09cdd1f94591873/sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", size = 2118415, upload-time = "2025-08-11T15:51:17.256Z" }, { url = "https://files.pythonhosted.org/packages/cf/ff/22ab2328148492c4d71899d62a0e65370ea66c877aea017a244a35733685/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", size = 3248707, upload-time = "2025-08-11T15:52:38.444Z" }, @@ -7517,6 +7892,7 @@ version = "0.50.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } wheels = [ @@ -7632,15 +8008,6 @@ test = [ { name = "pytest-timeout" }, ] -[[package]] -name = "tenacity" -version = "9.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, -] - [[package]] name = "tensorboard" version = "2.20.0" @@ -7649,8 +8016,8 @@ dependencies = [ { name = "absl-py" }, { name = "grpcio" }, { name = "markdown" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "packaging" }, { name = "pillow" }, { name = "protobuf" }, @@ -7677,13 +8044,17 @@ name = "tensorstore" version = "0.1.76" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ml-dtypes", version = "0.5.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 's390x' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "ml-dtypes", version = "0.5.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 's390x' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "ml-dtypes" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ff/ae/947a9f232de7319b664ed8d278e9e0363e9294da73fd422c687ac4eb070e/tensorstore-0.1.76.tar.gz", hash = "sha256:ed0d565e7a038a84b1b5b5d9f7397caec200b53941d8889f44b7f63dd6abffe7", size = 6869230, upload-time = "2025-07-02T21:34:03.773Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/09/37/f2254b4ae1dabd95e258fa3eb4783ac4db4261bb8c90ff9bfe15549d1238/tensorstore-0.1.76-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:b68450983ccad9e7774e81b2fa37daef1b72c774fd939d9eb4065d6aa70e666a", size = 15712650, upload-time = "2025-07-02T21:33:39.716Z" }, + { url = "https://files.pythonhosted.org/packages/93/3c/1cae56cbbe9610ff48cb2d7c0921a4d4c333a0540918e3b2db08b521c5f6/tensorstore-0.1.76-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b7a3856f884279e40f90bad87d0da70869879e124835e650c6b16c80f64fbc4", size = 13624138, upload-time = "2025-07-02T21:33:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d2/b92d34a896f608a59dc76c290d4ec9f7d0264a02e4d74864987a6adbd3c9/tensorstore-0.1.76-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8709a98ae0b453eb23525c07372c2be1f6bbd978bba53319f26a1f2a83a77c2a", size = 17538270, upload-time = "2025-07-02T21:33:44.911Z" }, + { url = "https://files.pythonhosted.org/packages/21/66/142b803541552b02a2fa033b1f48bcb50e1d2df6ac10131aab1857c5141d/tensorstore-0.1.76-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:267edea8f1596f2bd67017ff97b7b350bf3f95ff84947a8babadc5e17ca53663", size = 18910782, upload-time = "2025-07-02T21:33:47.401Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3e/c264cf1435c04fb998a1f30dd1f066deb370b841412f89e1cb36d37ee4fc/tensorstore-0.1.76-cp312-cp312-win_amd64.whl", hash = "sha256:f66ac63d0c63c3336ac4dc61f1f97b6afe8b512e586ddfdbc91f19175787f321", size = 12611059, upload-time = "2025-07-02T21:33:49.596Z" }, { url = "https://files.pythonhosted.org/packages/5f/66/1e3b819e1de98b048dad7843f3a814c5e739ead57f511dafb6aa0748f04a/tensorstore-0.1.76-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:a471994b156daa3cadb0e4968e29202fa2e8c7ddcd28d825499bb5637caa0983", size = 15713110, upload-time = "2025-07-02T21:33:51.973Z" }, { url = "https://files.pythonhosted.org/packages/58/d3/226344e8822c5e02af929c89bd61964e08980253cda15286a201850eb3b1/tensorstore-0.1.76-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:98175dc64935b49467cb7664a431b9a06e9df9b5cab94f9a1fdb24a30b2d69d3", size = 13624514, upload-time = "2025-07-02T21:33:54.109Z" }, { url = "https://files.pythonhosted.org/packages/94/9f/2b267c520dbbcf0a5ebc7a3c0a6cf852a445e22c8ea8b0f7450bf6b98783/tensorstore-0.1.76-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9e30577f1197ea3573102912482dced95e4c6ff72087ffeb99b5d8b496bf81a", size = 17539304, upload-time = "2025-07-02T21:33:56.172Z" }, @@ -7710,6 +8081,12 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/a7/86/ad0155a37c4f310935d5ac0b1ccf9bdb635dcb906e0a9a26b616dd55825a/tiktoken-0.11.0.tar.gz", hash = "sha256:3c518641aee1c52247c2b97e74d8d07d780092af79d5911a6ab5e79359d9b06a", size = 37648, upload-time = "2025-08-08T23:58:08.495Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9e/eceddeffc169fc75fe0fd4f38471309f11cb1906f9b8aa39be4f5817df65/tiktoken-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fd9e6b23e860973cf9526544e220b223c60badf5b62e80a33509d6d40e6c8f5d", size = 1055199, upload-time = "2025-08-08T23:57:45.076Z" }, + { url = "https://files.pythonhosted.org/packages/4f/cf/5f02bfefffdc6b54e5094d2897bc80efd43050e5b09b576fd85936ee54bf/tiktoken-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a76d53cee2da71ee2731c9caa747398762bda19d7f92665e882fef229cb0b5b", size = 996655, upload-time = "2025-08-08T23:57:46.304Z" }, + { url = "https://files.pythonhosted.org/packages/65/8e/c769b45ef379bc360c9978c4f6914c79fd432400a6733a8afc7ed7b0726a/tiktoken-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef72aab3ea240646e642413cb363b73869fed4e604dcfd69eec63dc54d603e8", size = 1128867, upload-time = "2025-08-08T23:57:47.438Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2d/4d77f6feb9292bfdd23d5813e442b3bba883f42d0ac78ef5fdc56873f756/tiktoken-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f929255c705efec7a28bf515e29dc74220b2f07544a8c81b8d69e8efc4578bd", size = 1183308, upload-time = "2025-08-08T23:57:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/7a/65/7ff0a65d3bb0fc5a1fb6cc71b03e0f6e71a68c5eea230d1ff1ba3fd6df49/tiktoken-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61f1d15822e4404953d499fd1dcc62817a12ae9fb1e4898033ec8fe3915fdf8e", size = 1244301, upload-time = "2025-08-08T23:57:49.642Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6e/5b71578799b72e5bdcef206a214c3ce860d999d579a3b56e74a6c8989ee2/tiktoken-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:45927a71ab6643dfd3ef57d515a5db3d199137adf551f66453be098502838b0f", size = 884282, upload-time = "2025-08-08T23:57:50.759Z" }, { url = "https://files.pythonhosted.org/packages/cc/cd/a9034bcee638716d9310443818d73c6387a6a96db93cbcb0819b77f5b206/tiktoken-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a5f3f25ffb152ee7fec78e90a5e5ea5b03b4ea240beed03305615847f7a6ace2", size = 1055339, upload-time = "2025-08-08T23:57:51.802Z" }, { url = "https://files.pythonhosted.org/packages/f1/91/9922b345f611b4e92581f234e64e9661e1c524875c8eadd513c4b2088472/tiktoken-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dc6e9ad16a2a75b4c4be7208055a1f707c9510541d94d9cc31f7fbdc8db41d8", size = 997080, upload-time = "2025-08-08T23:57:53.442Z" }, { url = "https://files.pythonhosted.org/packages/d0/9d/49cd047c71336bc4b4af460ac213ec1c457da67712bde59b892e84f1859f/tiktoken-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a0517634d67a8a48fd4a4ad73930c3022629a85a217d256a6e9b8b47439d1e4", size = 1128501, upload-time = "2025-08-08T23:57:54.808Z" }, @@ -7761,13 +8138,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684, upload-time = "2025-09-19T09:49:24.953Z" }, ] +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, +] + [[package]] name = "torch" version = "2.10.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "platform_machine != 's390x' and sys_platform == 'darwin'", - "platform_machine == 's390x' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version < '3.13' and sys_platform == 'darwin'", ] dependencies = [ { name = "filelock", marker = "sys_platform == 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, @@ -7780,11 +8166,17 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/abada41517ce0011775f0f4eacc79659bc9bc6c361e6bfe6f7052a6b9363/torch-2.10.0-3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6", size = 915622781, upload-time = "2026-03-11T14:17:11.354Z" }, { url = "https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b", size = 915627275, upload-time = "2026-03-11T14:16:11.325Z" }, { url = "https://files.pythonhosted.org/packages/d8/f0/72bf18847f58f877a6a8acf60614b14935e2f156d942483af1ffc081aea0/torch-2.10.0-3-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49", size = 915523474, upload-time = "2026-03-11T14:17:44.422Z" }, { url = "https://files.pythonhosted.org/packages/f4/39/590742415c3030551944edc2ddc273ea1fdfe8ffb2780992e824f1ebee98/torch-2.10.0-3-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b1d5e2aba4eb7f8e87fbe04f86442887f9167a35f092afe4c237dfcaaef6e328", size = 915632474, upload-time = "2026-03-11T14:15:13.666Z" }, { url = "https://files.pythonhosted.org/packages/b6/8e/34949484f764dde5b222b7fe3fede43e4a6f0da9d7f8c370bb617d629ee2/torch-2.10.0-3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:0228d20b06701c05a8f978357f657817a4a63984b0c90745def81c18aedfa591", size = 915523882, upload-time = "2026-03-11T14:14:46.311Z" }, + { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, + { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" }, { url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" }, { url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" }, { url = "https://files.pythonhosted.org/packages/6a/16/502fb1b41e6d868e8deb5b0e3ae926bbb36dab8ceb0d1b769b266ad7b0c3/torch-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57", size = 113757050, upload-time = "2026-01-21T16:24:19.204Z" }, @@ -7808,33 +8200,44 @@ name = "torch" version = "2.10.0+cu129" source = { registry = "https://download.pytorch.org/whl/cu129" } resolution-markers = [ - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", ] dependencies = [ { name = "cuda-bindings", version = "12.9.4", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, @@ -7859,11 +8262,12 @@ dependencies = [ { name = "nvidia-nvtx-cu12", marker = "sys_platform == 'linux' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "setuptools", marker = "sys_platform != 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "sympy", marker = "sys_platform != 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "triton", version = "3.6.0", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(platform_machine != 's390x' and sys_platform == 'linux') or (platform_machine == 's390x' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (platform_machine == 's390x' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (platform_machine == 's390x' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine == 's390x' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (platform_machine == 's390x' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine == 's390x' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine == 's390x' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (platform_machine == 's390x' and extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 's390x' and sys_platform == 'linux') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (platform_machine != 's390x' and extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'linux' and extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "triton", marker = "sys_platform == 'linux' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "typing-extensions", marker = "sys_platform != 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu129/torch-2.10.0%2Bcu129-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:895501ca59670503c00aeca56aa864fe59ebb11bdc919e779683d5ae263a171a" }, + { url = "https://download-r2.pytorch.org/whl/cu129/torch-2.10.0%2Bcu129-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a3c703e74a88cccfeb4b1807c49d563a0a73793ba72d4fa035d4ac5885f3aefd" }, { url = "https://download-r2.pytorch.org/whl/cu129/torch-2.10.0%2Bcu129-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:08dc9eb950efbf2b65a7973e6d00c8c770d697140296841dc3d86cbc8d372a76" }, { url = "https://download-r2.pytorch.org/whl/cu129/torch-2.10.0%2Bcu129-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e116126decbfbd1fc6f8e07c0d1527f014b0b787b50479d84592ccc44870f8d5" }, { url = "https://download-r2.pytorch.org/whl/cu129/torch-2.10.0%2Bcu129-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:426cd15b348547131a3de733056396dea0edfb511cd5e351a6ba9efa561dfb86" }, @@ -7879,11 +8283,15 @@ name = "torch-c-dlpack-ext" version = "0.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] 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" }, { url = "https://files.pythonhosted.org/packages/ca/c6/65346a201d921b616731311fc9941f15137672b444cebdad702cb52ccee0/torch_c_dlpack_ext-0.1.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:74acea2ed395cadda63342845b9e9ee7cd4537846223dacfb4431b4610109265", size = 1993243, upload-time = "2026-01-12T11:24:51.079Z" }, { url = "https://files.pythonhosted.org/packages/fd/ec/faf10be09a5812b1c5ec9922b53fb5def5fc4080b81a653b9347bb169ebb/torch_c_dlpack_ext-0.1.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49f1e99d13c64e22dac0a34a1560e9e5a398a49a9fa81df83053e04fde6ec5bd", size = 443798, upload-time = "2026-01-12T11:24:52.754Z" }, { url = "https://files.pythonhosted.org/packages/2d/68/f434b48700f3e04f33882f54d8d3910327b935f55e14ec49da7d607bf470/torch_c_dlpack_ext-0.1.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:debe62e5ef93e631065d6b9f6e60d3d39bae6b89fa1b25d9523f40b3efbf8aba", size = 755004, upload-time = "2026-01-12T11:24:54.004Z" }, @@ -7908,12 +8316,12 @@ name = "torchao" version = "0.9.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "platform_machine != 's390x' and sys_platform == 'linux'", - "platform_machine == 's390x' and sys_platform == 'linux'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux'", - "platform_machine != 's390x' and sys_platform == 'darwin'", - "platform_machine == 's390x' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version < '3.13' and sys_platform == 'linux'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version < '3.13' and sys_platform == 'darwin'", ] wheels = [ { url = "https://files.pythonhosted.org/packages/7d/fe/a24225d30775192a4c5d9cea3ecb95e6adc69d0a8b5ed98eb8e58d362344/torchao-0.9.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc708910301a9f98344d43f3fe2aa6d5e1fab706d772b6df47ff05087d664145", size = 5652091, upload-time = "2025-02-28T13:54:15.239Z" }, @@ -7925,13 +8333,14 @@ name = "torchao" version = "0.14.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "platform_machine == 'aarch64' and sys_platform == 'linux'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux'", - "platform_machine == 's390x' and sys_platform == 'linux'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux'", - "platform_machine != 's390x' and sys_platform == 'darwin'", - "platform_machine == 's390x' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version < '3.13' and sys_platform == 'darwin'", ] wheels = [ { url = "https://files.pythonhosted.org/packages/91/56/19abb32bbdc55d9fdebf8d6315a8f7d8ae10e387a91c631abd92afe0056b/torchao-0.14.1-cp310-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f68db5e41952e88daa383fc2f358541e617654f388f508d5c7580c3bee9447", size = 7197175, upload-time = "2025-10-24T01:02:59.223Z" }, @@ -7942,14 +8351,15 @@ wheels = [ name = "torchaudio" version = "2.10.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "platform_machine != 's390x' and sys_platform == 'darwin'", - "platform_machine == 's390x' and sys_platform == 'darwin'", -] dependencies = [ - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/36/28a6f3e857616cf7576bdbf8170e483b8c5d0a1f8d349ecb2b75921236aa/torchaudio-2.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d0fbdbfd2f621c51d28571050d6d0c7287791034e5c7303b31480af1258f33f", size = 737144, upload-time = "2026-01-21T16:28:44.189Z" }, + { url = "https://files.pythonhosted.org/packages/ea/3f/df620439a76ece170472d41438d11a1545d5db5dc9f1eaeab8c6e055a328/torchaudio-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:42b148a0921a3721abd1f6ae098b1ec9f89703e555c4f7a0d44da87b8decbcb9", size = 391973, upload-time = "2026-01-21T16:28:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/98/25/e55a30d7138f8fe56ed006df25b0a3c27681f0ec7bc9989e1778e6d559c3/torchaudio-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0e77b2956448d63790a99beed0b74ac8b8cd3a94dcdd9ad01974411078f46278", size = 1895234, upload-time = "2026-01-21T16:28:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/be/a0/da53c7d20fac15f66f8838653b91162de1bf21fb40fee88cf839e4ef5174/torchaudio-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f76a01ecebf1869e1f2c50a261f1cf07e5fccb24402b4e9bbb82d6725b9c7dd", size = 475470, upload-time = "2026-01-21T16:28:40.615Z" }, { url = "https://files.pythonhosted.org/packages/b6/02/341e7bd588355f82c5180103cb2f8070a72ab1be920ab27553a1135d4aa6/torchaudio-2.10.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:8fd38d28ee150c584d3ee3b05f39e021f0ad8a8ec8fec1f26dfe150c9db9b2f5", size = 737164, upload-time = "2026-01-21T16:28:38.354Z" }, { url = "https://files.pythonhosted.org/packages/49/fd/831c2595c81b17141180ca11ab3c0836cc544ef13e15aa0e7b2cb619e582/torchaudio-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5bc39ff3ea341097ce1ab023dd88c9dd8ca5f96ebf48821e7d23766137bb55d7", size = 392757, upload-time = "2026-01-21T16:28:33.631Z" }, { url = "https://files.pythonhosted.org/packages/8e/d8/405c80c57dc68ca5855bddfaae57c3d84ea7397bf1eb2aa5d59c9fa1d3a9/torchaudio-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3057c4286db5673d266124a2a10ca54e19f516772e9057f44573a7da5b85e328", size = 1897099, upload-time = "2026-01-21T16:28:24.793Z" }, @@ -7968,51 +8378,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/61/0e1f464463b85bc677036faffdfd23493aa17e8c3fc3a649abca8c019701/torchaudio-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e49f6a18a8552620c4394f8529b7551eda9312d46dfdd3500bd2be459c86aea4", size = 478968, upload-time = "2026-01-21T16:28:19.542Z" }, ] -[[package]] -name = "torchaudio" -version = "2.10.0+cu129" -source = { registry = "https://download.pytorch.org/whl/cu129" } -resolution-markers = [ - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", -] -dependencies = [ - { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "sys_platform != 'darwin'" }, -] -wheels = [ - { url = "https://download-r2.pytorch.org/whl/cu129/torchaudio-2.10.0%2Bcu129-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7da6d91ff23575a220202fa8b1db1531c2b126ad6cf7515f2e28afe95979ed55" }, - { url = "https://download-r2.pytorch.org/whl/cu129/torchaudio-2.10.0%2Bcu129-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:25f99b060c867bedae2d5520acc3504afca0e75255724abdc4e05142a6c537cd" }, - { url = "https://download-r2.pytorch.org/whl/cu129/torchaudio-2.10.0%2Bcu129-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:b64d03ab8ef46a158987a6943957c5cf537cc6fb8cb82e6eb80cd4eee1691b65" }, - { url = "https://download-r2.pytorch.org/whl/cu129/torchaudio-2.10.0%2Bcu129-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:411826a1d33e62404b69908242a017820e62f5a05facd277efc225a90a409570" }, - { url = "https://download-r2.pytorch.org/whl/cu129/torchaudio-2.10.0%2Bcu129-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:1e949dcec56d58469a80c9f4cb0d2ed377cb3b88512ff94cec4a2c5a167a6aa6" }, - { url = "https://download-r2.pytorch.org/whl/cu129/torchaudio-2.10.0%2Bcu129-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:37bec8b2993d76f4ad5f810f12eb46f8d2e74fcfc9047fe7b157922f0d6e3b7d" }, - { url = "https://download-r2.pytorch.org/whl/cu129/torchaudio-2.10.0%2Bcu129-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:1de7f85a39861093178ee425301023f85f7c3b3ce01667a6279f24e848658b84" }, - { url = "https://download-r2.pytorch.org/whl/cu129/torchaudio-2.10.0%2Bcu129-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c87e8bc610ef9c77456edaefa29d0ada3f4b8e1e903f735ec4eb48a4d91ec8f6" }, -] - [[package]] name = "torchcodec" -version = "0.9.1" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/c7/67fc8417f9efa8a25c00a44f0d674761a0bad9c45e9725e3fd116b3c48ed/torchcodec-0.9.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5c9cdcba50c75be70ef6ec919ec1f7f14d9d5163d93cf6bd94403e134f03734c", size = 4034415, upload-time = "2025-12-10T15:56:02.04Z" }, - { url = "https://files.pythonhosted.org/packages/68/05/06240f661e9aa08b20765305e3b88f60bff706bbe54ac35830af74612443/torchcodec-0.9.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0643c5e9c3a51fdafdea87935d5b0a38e99626c664f47a150482d77ab370a877", size = 2067767, upload-time = "2025-12-10T15:55:37.27Z" }, - { url = "https://files.pythonhosted.org/packages/13/a2/d78cd65863fb805d9e35fe90ae7574eab86ff0ae63438208bd07d2cf1fd2/torchcodec-0.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:df0b5a15998fd7457625c2af2a6276e0e710fac158d145045340dbbcd1cfdb65", size = 2186788, upload-time = "2025-12-10T15:56:20.204Z" }, - { url = "https://files.pythonhosted.org/packages/01/02/f8ae9443d3bcbe8a8d6d0bbc3992296e5476e5afa1f244100a3a7967a36c/torchcodec-0.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b9bc5a5dff925df96d11bf90bd0ce964b8086bb11ae09adf353518192b5da483", size = 3812248, upload-time = "2025-12-10T15:56:06.382Z" }, - { url = "https://files.pythonhosted.org/packages/59/a1/8462b55571286847ea31edb7634583125400824267db9ba8301f4ce3f137/torchcodec-0.9.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:65634bb28b3155cf99f980dac31ecedb414c07b8156f8473ec9fb74bedbd2a1f", size = 2068456, upload-time = "2025-12-10T15:55:40.577Z" }, - { url = "https://files.pythonhosted.org/packages/f2/63/752d0fc1c6e8f799ae880ca1087510def663a7f9aa1a70074ae334c6908f/torchcodec-0.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:2d01c8b3685a3a38f050ed2b526808a2938dba6f56cb9f9e967884fd858bba15", size = 2188320, upload-time = "2025-12-10T15:56:24.63Z" }, + { url = "https://files.pythonhosted.org/packages/17/ae/8b1d69e653894243fa66e2fec511cf203107dd146d161c9f095893c13bbc/torchcodec-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af82d1fac3667335e089dc958b5e8eef5458e37d65cb3a94ebf81f45f00f7805", size = 3903714, upload-time = "2025-10-16T14:42:53.127Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fd/eec92c82545038a90ffd24e3626bb3a85f7d51577b04819c1c753d380a9b/torchcodec-0.8.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:2ec2e874dfb6fbf9bbeb792bea56317529636e78db175f56aad1e4efd6e12502", size = 1898382, upload-time = "2025-10-16T14:43:37.699Z" }, + { url = "https://files.pythonhosted.org/packages/fe/09/ce7436151a3825f27c00263d722b0cf093609921da6cf24b0fa8133cc415/torchcodec-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:318da9af9179d156be0a84296e909d51e4cd758598eaaea08c828790c80bf977", size = 2070488, upload-time = "2025-10-16T14:43:21.803Z" }, + { url = "https://files.pythonhosted.org/packages/9b/1c/40fd9358e5dd958775b8d0a01c962a022884810f441ac28229ed0e811599/torchcodec-0.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:1f3309252d035c888e6ae4518f5aca24f1c38f163124792d8a29a6872bf457f2", size = 3873235, upload-time = "2025-10-16T14:42:54.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/81/2e8f8657aed983f20f9ce842b19016d4aff05dd608ac0def94e013602814/torchcodec-0.8.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:253cc3c7a17c7be26abfcf2470e8eab3803ff3108f70be060a7efdcb49d917bc", size = 1902114, upload-time = "2025-10-16T14:43:39.112Z" }, + { url = "https://files.pythonhosted.org/packages/09/1f/b09f028822991241eb1a31931749d034aee2c654d00f1930f4cecce595bc/torchcodec-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:c69285cb393c3b36c7bcc4e59e304076ea22b350ff6adca4a2a09b5f3f81f15c", size = 2070381, upload-time = "2025-10-16T14:43:22.942Z" }, ] [[package]] @@ -8034,8 +8410,8 @@ name = "torchprofile" version = "0.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "sys_platform != 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, @@ -8051,16 +8427,20 @@ name = "torchvision" version = "0.25.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "platform_machine != 's390x' and sys_platform == 'darwin'", - "platform_machine == 's390x' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version < '3.13' and sys_platform == 'darwin'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-automodel') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "pillow", marker = "sys_platform == 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/56/3a/6ea0d73f49a9bef38a1b3a92e8dd455cea58470985d25635beab93841748/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2abe430c90b1d5e552680037d68da4eb80a5852ebb1c811b2b89d299b10573b", size = 1874920, upload-time = "2026-01-21T16:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/51/f8/c0e1ef27c66e15406fece94930e7d6feee4cb6374bbc02d945a630d6426e/torchvision-0.25.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b75deafa2dfea3e2c2a525559b04783515e3463f6e830cb71de0fb7ea36fe233", size = 2344556, upload-time = "2026-01-21T16:27:40.125Z" }, + { url = "https://files.pythonhosted.org/packages/68/2f/f24b039169db474e8688f649377de082a965fbf85daf4e46c44412f1d15a/torchvision-0.25.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f25aa9e380865b11ea6e9d99d84df86b9cc959f1a007cd966fc6f1ab2ed0e248", size = 8072351, upload-time = "2026-01-21T16:27:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/16/8f650c2e288977cf0f8f85184b90ee56ed170a4919347fc74ee99286ed6f/torchvision-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9c55ae8d673ab493325d1267cbd285bb94d56f99626c00ac4644de32a59ede3", size = 4303059, upload-time = "2026-01-21T16:27:11.08Z" }, { url = "https://files.pythonhosted.org/packages/f5/5b/1562a04a6a5a4cf8cf40016a0cdeda91ede75d6962cff7f809a85ae966a5/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:24e11199e4d84ba9c5ee7825ebdf1cd37ce8deec225117f10243cae984ced3ec", size = 1874918, upload-time = "2026-01-21T16:27:39.02Z" }, { url = "https://files.pythonhosted.org/packages/36/b1/3d6c42f62c272ce34fcce609bb8939bdf873dab5f1b798fd4e880255f129/torchvision-0.25.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f271136d2d2c0b7a24c5671795c6e4fd8da4e0ea98aeb1041f62bc04c4370ef", size = 2309106, upload-time = "2026-01-21T16:27:30.624Z" }, { url = "https://files.pythonhosted.org/packages/c7/60/59bb9c8b67cce356daeed4cb96a717caa4f69c9822f72e223a0eae7a9bd9/torchvision-0.25.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:855c0dc6d37f462482da7531c6788518baedca1e0847f3df42a911713acdfe52", size = 8071522, upload-time = "2026-01-21T16:27:29.392Z" }, @@ -8084,41 +8464,54 @@ name = "torchvision" version = "0.25.0+cu129" source = { registry = "https://download.pytorch.org/whl/cu129" } resolution-markers = [ - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-automodel') or (sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-automodel') or (sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-mcore') or (sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "pillow", marker = "sys_platform != 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "sys_platform != 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, ] wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu129/torchvision-0.25.0%2Bcu129-cp312-cp312-manylinux_2_28_aarch64.whl" }, + { url = "https://download-r2.pytorch.org/whl/cu129/torchvision-0.25.0%2Bcu129-cp312-cp312-manylinux_2_28_x86_64.whl" }, { url = "https://download-r2.pytorch.org/whl/cu129/torchvision-0.25.0%2Bcu129-cp313-cp313-manylinux_2_28_aarch64.whl" }, { url = "https://download-r2.pytorch.org/whl/cu129/torchvision-0.25.0%2Bcu129-cp313-cp313-manylinux_2_28_x86_64.whl" }, { url = "https://download-r2.pytorch.org/whl/cu129/torchvision-0.25.0%2Bcu129-cp313-cp313t-manylinux_2_28_aarch64.whl" }, @@ -8152,53 +8545,76 @@ wheels = [ [[package]] name = "transformer-engine" -version = "2.14.0+71bbefb" -source = { git = "https://github.com/NVIDIA/TransformerEngine.git?rev=71bbefbf153418f943640df0f7373625dc93fa46#71bbefbf153418f943640df0f7373625dc93fa46" } +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/1d/831c836ceb550b2273a68b25948dab870d4c8b3d62860855d5736be8829d/transformer_engine-2.12.0-py3-none-any.whl", hash = "sha256:5d0539c520c39445c62feab9d1fab774ed2e27a576d0feac2086ce86e0bff7c4", size = 739825, upload-time = "2026-02-21T06:04:03.335Z" }, +] + +[package.optional-dependencies] +pytorch = [ + { name = "transformer-engine-torch" }, +] + +[[package]] +name = "transformer-engine-cu12" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "packaging" }, { name = "pydantic" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/1a/3170941ab13fb230aaadb0b27b31ae3430d5cddb7a41ea0ee4891b0d15df/transformer_engine_cu12-2.12.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:4a9764526581adc8968aab21e22681a4690847c9e087a39df8ea21ceca0a4fe5", size = 311258513, upload-time = "2026-02-21T06:04:19.627Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/cd797712c9d0dad83d4874a84acac9f80d2b8e64bdaea800bfa3ea94787f/transformer_engine_cu12-2.12.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:8bfb8137e17fb1d93d86d92d3c58dce14754d37b61263dc6cc3abac4165b1c03", size = 311834847, upload-time = "2026-02-21T06:04:18.865Z" }, +] + +[[package]] +name = "transformer-engine-torch" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "einops" }, + { name = "nvdlfw-inspect" }, + { name = "onnx" }, + { name = "onnxscript" }, + { name = "packaging" }, + { name = "pydantic" }, { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "sys_platform != 'darwin' or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "transformer-engine-cu12" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/b0/aa/5872f0944e88b9a7a0074f207178e18d183916bc05887bf1d76947769d94/transformer_engine_torch-2.12.0.tar.gz", hash = "sha256:ca12cb27fa8b68818f2f11c5c4147c62ffec3d30f9db61642ea3f1b3baf3d428", size = 268222, upload-time = "2026-02-21T06:04:15.181Z" } [[package]] name = "transformers" -version = "5.3.0" +version = "5.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "packaging" }, { name = "pyyaml" }, { name = "regex" }, { name = "safetensors" }, { name = "tokenizers" }, { name = "tqdm" }, - { name = "typer" }, + { name = "typer-slim" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/1a/70e830d53ecc96ce69cfa8de38f163712d2b43ac52fbd743f39f56025c31/transformers-5.3.0.tar.gz", hash = "sha256:009555b364029da9e2946d41f1c5de9f15e6b1df46b189b7293f33a161b9c557", size = 8830831, upload-time = "2026-03-04T17:41:46.119Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/7e/8a0c57d562015e5b16c97c1f0b8e0e92ead2c7c20513225dc12c2043ba9f/transformers-5.2.0.tar.gz", hash = "sha256:0088b8b46ccc9eff1a1dca72b5d618a5ee3b1befc3e418c9512b35dea9f9a650", size = 8618176, upload-time = "2026-02-16T18:54:02.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/88/ae8320064e32679a5429a2c9ebbc05c2bf32cefb6e076f9b07f6d685a9b4/transformers-5.3.0-py3-none-any.whl", hash = "sha256:50ac8c89c3c7033444fb3f9f53138096b997ebb70d4b5e50a2e810bf12d3d29a", size = 10661827, upload-time = "2026-03-04T17:41:42.722Z" }, + { url = "https://files.pythonhosted.org/packages/4e/93/79754b0ca486e556c2b95d4f5afc66aaf4b260694f3d6e1b51da2d036691/transformers-5.2.0-py3-none-any.whl", hash = "sha256:9ecaf243dc45bee11a7d93f8caf03746accc0cb069181bbf4ad8566c53e854b4", size = 10403304, upload-time = "2026-02-16T18:53:59.699Z" }, ] [[package]] name = "triton" version = "3.6.0" source = { registry = "https://download.pytorch.org/whl/cu129" } -resolution-markers = [ - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra != 'extra-7-nemo-rl-vllm'", - "platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", - "platform_machine != 'aarch64' and platform_machine != 's390x' and sys_platform == 'linux' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang'", -] wheels = [ + { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e42d084864d12b8784736fe51a0cd05f6cc56775b25c8102c9d80c421f4e298" }, + { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f5928e6d44c34a97bbe164cceddc0ef2007121c89ebcfba5415cf452de7ee9f" }, { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58d57d6796b0004076315433526fe9d4af42044d430afdee1e6cd42a76bd6d09" }, { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0075039ff27765480083b1a109999bf27110f3542f1f9fad95f0f9065a36da79" }, { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ff9ec4c96f405bf2735c531af27f8cc2215927440e52fb1fbc8014961d5d12" }, @@ -8209,24 +8625,6 @@ wheels = [ { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:621c0e5973d834507ec69dcf909f35f0b02c5df490fde100433cef83359b7eeb" }, ] -[[package]] -name = "triton" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "platform_machine == 's390x' and sys_platform == 'linux'", -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" }, - { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4e/41b0c8033b503fd3cfcd12392cdd256945026a91ff02452bef40ec34bee7/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6", size = 176276087, upload-time = "2026-01-20T16:16:18.989Z" }, - { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, - { url = "https://files.pythonhosted.org/packages/49/55/5ecf0dcaa0f2fbbd4420f7ef227ee3cb172e91e5fede9d0ecaddc43363b4/triton-3.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43", size = 176138577, upload-time = "2026-01-20T16:16:25.426Z" }, - { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, - { url = "https://files.pythonhosted.org/packages/48/db/56ee649cab5eaff4757541325aca81f52d02d4a7cd3506776cad2451e060/triton-3.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d", size = 176274804, upload-time = "2026-01-20T16:16:31.528Z" }, - { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, -] - [[package]] name = "trove-classifiers" version = "2025.8.6.13" @@ -8332,28 +8730,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] -[[package]] -name = "uuid-utils" -version = "0.14.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, - { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, - { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, - { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, - { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, - { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, - { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, - { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, - { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, - { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, - { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, -] - [[package]] name = "uvicorn" version = "0.35.0" @@ -8384,6 +8760,12 @@ version = "0.21.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284, upload-time = "2024-10-14T23:37:47.833Z" }, + { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349, upload-time = "2024-10-14T23:37:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089, upload-time = "2024-10-14T23:37:51.703Z" }, + { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770, upload-time = "2024-10-14T23:37:54.122Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321, upload-time = "2024-10-14T23:37:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022, upload-time = "2024-10-14T23:37:58.195Z" }, { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" }, { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" }, { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" }, @@ -8408,7 +8790,7 @@ wheels = [ [[package]] name = "vllm" -version = "0.17.1" +version = "0.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -8421,7 +8803,7 @@ dependencies = [ { name = "depyf" }, { name = "diskcache" }, { name = "einops" }, - { name = "fastapi", extra = ["standard"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "fastapi", extra = ["standard"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "filelock" }, { name = "flashinfer-python" }, { name = "gguf" }, @@ -8433,13 +8815,12 @@ dependencies = [ { name = "llguidance" }, { name = "lm-format-enforcer" }, { name = "mcp" }, - { name = "mistral-common", extra = ["image"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "mistral-common", extra = ["image"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "model-hosting-container-standards" }, { name = "msgspec" }, { name = "ninja" }, { name = "numba" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "nvidia-cudnn-frontend" }, { name = "nvidia-cutlass-dsl" }, { name = "openai" }, { name = "openai-harmony" }, @@ -8461,8 +8842,8 @@ dependencies = [ { name = "python-json-logger" }, { name = "pyyaml" }, { name = "pyzmq" }, - { name = "quack-kernels" }, - { name = "ray", extra = ["cgraph"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "quack-kernels", version = "0.2.10", source = { registry = "https://pypi.org/simple" } }, + { name = "ray", extra = ["cgraph"], marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "regex" }, { name = "requests" }, { name = "sentencepiece" }, @@ -8471,22 +8852,21 @@ dependencies = [ { name = "six" }, { name = "tiktoken" }, { name = "tokenizers" }, - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torchaudio", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torchaudio", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torchvision", version = "0.25.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torchaudio" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torchvision", version = "0.25.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "tqdm" }, { name = "transformers" }, { name = "typing-extensions" }, { name = "watchfiles" }, - { name = "xgrammar" }, + { name = "xgrammar", version = "0.1.29", source = { registry = "https://pypi.org/simple" }, 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/4c/7c/79ef306c71f3de2e73beb3c03c3f2966f966df61b8dd1f01dbdc65184050/vllm-0.17.1.tar.gz", hash = "sha256:d26a95dcb92e2ff78ed4b48bff247d845b0c768edf6c0acf2401376a56c57b61", size = 30547577, upload-time = "2026-03-11T11:03:58.693Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/d5/af83a4262ca4d5692a93b3c322ae954e3e6c4e23f8f9db3ab87bd79c919e/vllm-0.17.0.tar.gz", hash = "sha256:b0b62e58ef4eb633ef371f2726976372cf6dfcb7ff2ea9ddf7194c1930d5629a", size = 30541311, upload-time = "2026-03-07T03:54:54.333Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/02/ff63919abb341b0819f33a400c83698d095e5fd461ae3e44f3ff91f6489f/vllm-0.17.1-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:f04d63a94d0415b2323b0a0d3ab89a8d4d9bd346251ff60d47a7df679f7b3ff8", size = 385333057, upload-time = "2026-03-11T11:06:44.106Z" }, - { url = "https://files.pythonhosted.org/packages/18/28/f85e67b390082481298e27a5c9f1da540d2d5abb1a06a594545cdc320818/vllm-0.17.1-cp38-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:c52e892309532b4e51cb94d022c5e3c0087300cdb56e4645708601443299d871", size = 432931666, upload-time = "2026-03-11T11:06:00.955Z" }, + { url = "https://files.pythonhosted.org/packages/f2/72/78a48668f2631def18bbaaa331d7878bcfc5c3137455422aafb0748e1261/vllm-0.17.0-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:310fb82fe061ed75dceeb4aeb803cd8ee0d590337ec720f7abfb03a69314d710", size = 385329399, upload-time = "2026-03-07T03:54:34.261Z" }, + { url = "https://files.pythonhosted.org/packages/25/4f/972726f9a501f01203b5c4796e1932abbe435fae6d7715a4c3f1aad14a58/vllm-0.17.0-cp38-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:0296670a09d392ee43455d9bebf590d05a9bc2ebce5e25e2919222fc815158da", size = 432927988, upload-time = "2026-03-07T03:54:02.312Z" }, ] [[package]] @@ -8500,7 +8880,7 @@ wheels = [ [[package]] name = "wandb" -version = "0.25.1" +version = "0.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -8514,17 +8894,17 @@ dependencies = [ { name = "sentry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/bb/eb579bf9abac70934a014a9d4e45346aab307994f3021d201bebe5fa25ec/wandb-0.25.1.tar.gz", hash = "sha256:b2a95cd777ecbe7499599a43158834983448a0048329bc7210ef46ca18d21994", size = 43983308, upload-time = "2026-03-10T23:51:44.227Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/60/d94952549920469524b689479c864c692ca47eca4b8c2fe3389b64a58778/wandb-0.25.0.tar.gz", hash = "sha256:45840495a288e34245d69d07b5a0b449220fbc5b032e6b51c4f92ec9026d2ad1", size = 43951335, upload-time = "2026-02-13T00:17:45.515Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/d8/873553b6818499d1b1de314067d528b892897baf0dc81fedc0e845abc2dd/wandb-0.25.1-py3-none-macosx_12_0_arm64.whl", hash = "sha256:9bb0679a3e2dcd96db9d9b6d3e17d046241d8d122974b24facb85cc93309a8c9", size = 23615900, upload-time = "2026-03-10T23:51:06.278Z" }, - { url = "https://files.pythonhosted.org/packages/71/ea/b131f319aaa5d0bf7572b6bfcff3dd89e1cf92b17eee443bbab71d12d74c/wandb-0.25.1-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:0fb13ed18914027523e7b4fc20380c520e0d10da0ee452f924a13f84509fbe12", size = 25576144, upload-time = "2026-03-10T23:51:11.527Z" }, - { url = "https://files.pythonhosted.org/packages/70/5f/81508581f0bb77b0495665c1c78e77606a48e66e855ca71ba7c8ae29efa4/wandb-0.25.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:cc4521eb5223429ddab5e8eee9b42fdf4caabdf0bc4e0e809042720e5fbef0ed", size = 23070425, upload-time = "2026-03-10T23:51:15.71Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c7/445155ef010e2e35d190797d7c36ff441e062a5b566a6da4778e22233395/wandb-0.25.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:e73b4c55b947edae349232d5845204d30fac88e18eb4ad1d4b96bf7cf898405a", size = 25628142, upload-time = "2026-03-10T23:51:19.326Z" }, - { url = "https://files.pythonhosted.org/packages/d5/63/f5c55ee00cf481ef1ccd3c385a0585ad52e7840d08419d4f82ddbeeea959/wandb-0.25.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:22b84065aa398e1624d2e5ad79e08bc4d2af41a6db61697b03b3aaba332977c6", size = 23123172, upload-time = "2026-03-10T23:51:23.418Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d9/19eb7974c0e9253bcbaee655222c0f0e1a52e63e9479ee711b4208f8ac31/wandb-0.25.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:005c4c6b5126ef8f4b4110e5372d950918b00637d6dc4b615ad17445f9739478", size = 25714479, upload-time = "2026-03-10T23:51:27.421Z" }, - { url = "https://files.pythonhosted.org/packages/11/19/466c1d03323a4a0ed7d4036a59b18d6b6f67cb5032e444205927e226b18d/wandb-0.25.1-py3-none-win32.whl", hash = "sha256:8f2d04f16b88d65bfba9d79fb945f6c64e2686215469a841936e0972be8ec6a5", size = 24967338, upload-time = "2026-03-10T23:51:31.833Z" }, - { url = "https://files.pythonhosted.org/packages/89/22/680d34c1587f3a979c701b66d71aa7c42b4ef2fdf0774f67034e618e834e/wandb-0.25.1-py3-none-win_amd64.whl", hash = "sha256:62db5166de14456156d7a85953a58733a631228e6d4248a753605f75f75fb845", size = 24967343, upload-time = "2026-03-10T23:51:36.026Z" }, - { url = "https://files.pythonhosted.org/packages/c4/e8/76836b75d401ff5912aaf513176e64557ceaec4c4946bfd38a698ff84d48/wandb-0.25.1-py3-none-win_arm64.whl", hash = "sha256:cc7c34b70cf4b7be4d395541e82e325fd9d2be978d62c9ec01f1a7141523b6bb", size = 22080774, upload-time = "2026-03-10T23:51:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7d/0c131db3ec9deaabbd32263d90863cbfbe07659527e11c35a5c738cecdc5/wandb-0.25.0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:5eecb3c7b5e60d1acfa4b056bfbaa0b79a482566a9db58c9f99724b3862bc8e5", size = 23287536, upload-time = "2026-02-13T00:17:20.265Z" }, + { url = "https://files.pythonhosted.org/packages/c3/95/31bb7f76a966ec87495e5a72ac7570685be162494c41757ac871768dbc4f/wandb-0.25.0-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:daeedaadb183dc466e634fba90ab2bab1d4e93000912be0dee95065a0624a3fd", size = 25196062, upload-time = "2026-02-13T00:17:23.356Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a1/258cdedbf30cebc692198a774cf0ef945b7ed98ee64bdaf62621281c95d8/wandb-0.25.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:5e0127dbcef13eea48f4b84268da7004d34d3120ebc7b2fa9cefb72b49dbb825", size = 22799744, upload-time = "2026-02-13T00:17:26.437Z" }, + { url = "https://files.pythonhosted.org/packages/de/91/ec9465d014cfd199c5b2083d271d31b3c2aedeae66f3d8a0712f7f54bdf3/wandb-0.25.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:6c4c38077836f9b7569a35b0e1dcf1f0c43616fcd936d182f475edbfea063665", size = 25262839, upload-time = "2026-02-13T00:17:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/cb2d1c7143f534544147fb53fe87944508b8cb9a058bc5b6f8a94adbee15/wandb-0.25.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6edd8948d305cb73745bf564b807bd73da2ccbd47c548196b8a362f7df40aed8", size = 22853714, upload-time = "2026-02-13T00:17:31.68Z" }, + { url = "https://files.pythonhosted.org/packages/d7/94/68163f70c1669edcf130822aaaea782d8198b5df74443eca0085ec596774/wandb-0.25.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ada6f08629bb014ad6e0a19d5dec478cdaa116431baa3f0a4bf4ab8d9893611f", size = 25358037, upload-time = "2026-02-13T00:17:34.676Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fb/9578eed2c01b2fc6c8b693da110aa9c73a33d7bb556480f5cfc42e48c94e/wandb-0.25.0-py3-none-win32.whl", hash = "sha256:020b42ca4d76e347709d65f59b30d4623a115edc28f462af1c92681cb17eae7c", size = 24604118, upload-time = "2026-02-13T00:17:37.641Z" }, + { url = "https://files.pythonhosted.org/packages/25/97/460f6cb738aaa39b4eb2e6b4c630b2ae4321cdd70a79d5955ea75a878981/wandb-0.25.0-py3-none-win_amd64.whl", hash = "sha256:78307ac0b328f2dc334c8607bec772851215584b62c439eb320c4af4fb077a00", size = 24604122, upload-time = "2026-02-13T00:17:39.991Z" }, + { url = "https://files.pythonhosted.org/packages/27/6c/5847b4dda1dfd52630dac08711d4348c69ed657f0698fc2d949c7f7a6622/wandb-0.25.0-py3-none-win_arm64.whl", hash = "sha256:c6174401fd6fb726295e98d57b4231c100eca96bd17de51bfc64038a57230aaf", size = 21785298, upload-time = "2026-02-13T00:17:42.475Z" }, ] [[package]] @@ -8536,6 +8916,19 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406, upload-time = "2025-06-15T19:06:59.42Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/b8/858957045a38a4079203a33aaa7d23ea9269ca7761c8a074af3524fbb240/watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179", size = 402339, upload-time = "2025-06-15T19:05:24.516Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/98b222cca751ba68e88521fabd79a4fab64005fc5976ea49b53fa205d1fa/watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5", size = 394409, upload-time = "2025-06-15T19:05:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/50/dee79968566c03190677c26f7f47960aff738d32087087bdf63a5473e7df/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297", size = 450939, upload-time = "2025-06-15T19:05:26.494Z" }, + { url = "https://files.pythonhosted.org/packages/40/45/a7b56fb129700f3cfe2594a01aa38d033b92a33dddce86c8dfdfc1247b72/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0", size = 457270, upload-time = "2025-06-15T19:05:27.466Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c8/fa5ef9476b1d02dc6b5e258f515fcaaecf559037edf8b6feffcbc097c4b8/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e", size = 483370, upload-time = "2025-06-15T19:05:28.548Z" }, + { url = "https://files.pythonhosted.org/packages/98/68/42cfcdd6533ec94f0a7aab83f759ec11280f70b11bfba0b0f885e298f9bd/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee", size = 598654, upload-time = "2025-06-15T19:05:29.997Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/b2a1544224118cc28df7e59008a929e711f9c68ce7d554e171b2dc531352/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd", size = 478667, upload-time = "2025-06-15T19:05:31.172Z" }, + { url = "https://files.pythonhosted.org/packages/8c/77/e3362fe308358dc9f8588102481e599c83e1b91c2ae843780a7ded939a35/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f", size = 452213, upload-time = "2025-06-15T19:05:32.299Z" }, + { url = "https://files.pythonhosted.org/packages/6e/17/c8f1a36540c9a1558d4faf08e909399e8133599fa359bf52ec8fcee5be6f/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4", size = 626718, upload-time = "2025-06-15T19:05:33.415Z" }, + { url = "https://files.pythonhosted.org/packages/26/45/fb599be38b4bd38032643783d7496a26a6f9ae05dea1a42e58229a20ac13/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f", size = 623098, upload-time = "2025-06-15T19:05:34.534Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/fdf40e038475498e160cd167333c946e45d8563ae4dd65caf757e9ffe6b4/watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd", size = 279209, upload-time = "2025-06-15T19:05:35.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d3/3ae9d5124ec75143bdf088d436cba39812122edc47709cd2caafeac3266f/watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47", size = 292786, upload-time = "2025-06-15T19:05:36.559Z" }, + { url = "https://files.pythonhosted.org/packages/26/2f/7dd4fc8b5f2b34b545e19629b4a018bfb1de23b3a496766a2c1165ca890d/watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6", size = 284343, upload-time = "2025-06-15T19:05:37.5Z" }, { url = "https://files.pythonhosted.org/packages/d3/42/fae874df96595556a9089ade83be34a2e04f0f11eb53a8dbf8a8a5e562b4/watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30", size = 402004, upload-time = "2025-06-15T19:05:38.499Z" }, { url = "https://files.pythonhosted.org/packages/fa/55/a77e533e59c3003d9803c09c44c3651224067cbe7fb5d574ddbaa31e11ca/watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a", size = 393671, upload-time = "2025-06-15T19:05:39.52Z" }, { url = "https://files.pythonhosted.org/packages/05/68/b0afb3f79c8e832e6571022611adbdc36e35a44e14f129ba09709aa4bb7a/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc", size = 449772, upload-time = "2025-06-15T19:05:40.897Z" }, @@ -8608,8 +9001,8 @@ version = "1.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "braceexpand" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-nemo-rl-automodel' or extra == 'extra-7-nemo-rl-mcore' or extra == 'extra-7-nemo-rl-sglang' or extra != 'extra-7-nemo-rl-vllm'" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/3a/68800d92e065cf4750ebecf973b13979c0c929b439e1293012938862038d/webdataset-1.0.2.tar.gz", hash = "sha256:7f0498be827cfa46cc5430a58768a24e2c6a410676a61be1838f53d61afdaab4", size = 80090, upload-time = "2025-06-19T23:26:21.945Z" } @@ -8617,12 +9010,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/00/aca6beb3658dab4ed3dbb41a78e6e7f31342e0b41d28088f205525751601/webdataset-1.0.2-py3-none-any.whl", hash = "sha256:3dbfced32b25c0d199c6b9787937b6f85742bc3c84f652c846893075c1c082d9", size = 74956, upload-time = "2025-06-19T23:26:20.354Z" }, ] +[[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 = "15.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, @@ -8682,6 +9095,16 @@ version = "1.17.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, @@ -8727,15 +9150,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, ] -[[package]] -name = "wurlitzer" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/90/623f99c55c7d0727a58eb2b7dfb65cb406c561a5c2e9a95b0d6a450c473d/wurlitzer-3.1.1.tar.gz", hash = "sha256:bfb9144ab9f02487d802b9ff89dbd3fa382d08f73e12db8adc4c2fb00cd39bd9", size = 11867, upload-time = "2024-06-12T10:27:30.089Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/24/93ce54550a9dd3fd996ed477f00221f215bf6da3580397fbc138d6036e2e/wurlitzer-3.1.1-py3-none-any.whl", hash = "sha256:0b2749c2cde3ef640bf314a9f94b24d929fe1ca476974719a6909dfc568c3aac", size = 8590, upload-time = "2024-06-12T10:27:28.787Z" }, -] - [[package]] name = "xattr" version = "1.3.0" @@ -8745,6 +9159,13 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/08/d5/25f7b19af3a2cb4000cac4f9e5525a40bec79f4f5d0ac9b517c0544586a0/xattr-1.3.0.tar.gz", hash = "sha256:30439fabd7de0787b27e9a6e1d569c5959854cb322f64ce7380fedbfa5035036", size = 17148, upload-time = "2025-10-13T22:16:47.353Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/78/00bdc9290066173e53e1e734d8d8e1a84a6faa9c66aee9df81e4d9aeec1c/xattr-1.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dd4e63614722d183e81842cb237fd1cc978d43384166f9fe22368bfcb187ebe5", size = 23476, upload-time = "2025-10-13T22:16:06.942Z" }, + { url = "https://files.pythonhosted.org/packages/53/16/5243722294eb982514fa7b6b87a29dfb7b29b8e5e1486500c5babaf6e4b3/xattr-1.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:995843ef374af73e3370b0c107319611f3cdcdb6d151d629449efecad36be4c4", size = 18556, upload-time = "2025-10-13T22:16:08.209Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/d7ab0e547bea885b55f097206459bd612cefb652c5fc1f747130cbc0d42c/xattr-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa23a25220e29d956cedf75746e3df6cc824cc1553326d6516479967c540e386", size = 18869, upload-time = "2025-10-13T22:16:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/25/25cc7d64f07de644b7e9057842227adf61017e5bcfe59a79df79f768874c/xattr-1.3.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b4345387087fffcd28f709eb45aae113d911e1a1f4f0f70d46b43ba81e69ccdd", size = 38797, upload-time = "2025-10-13T22:16:11.624Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/cc350bcdbed006dfcc6ade0ac817693b8b3d4b2787f20e427fd0697042e4/xattr-1.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe92bb05eb849ab468fe13e942be0f8d7123f15d074f3aba5223fad0c4b484de", size = 38956, upload-time = "2025-10-13T22:16:13.121Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b2/9416317ac89e2ed759a861857cda0d5e284c3691e6f460d36cc2bd5ce4d1/xattr-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6c42ef5bdac3febbe28d3db14d3a8a159d84ba5daca2b13deae6f9f1fc0d4092", size = 38214, upload-time = "2025-10-13T22:16:14.389Z" }, + { url = "https://files.pythonhosted.org/packages/38/63/188f7cb41ab35d795558325d5cc8ab552171d5498cfb178fd14409651e18/xattr-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2aaa5d66af6523332189108f34e966ca120ff816dfa077ca34b31e6263f8a236", size = 37754, upload-time = "2025-10-13T22:16:15.306Z" }, { url = "https://files.pythonhosted.org/packages/27/d3/6a1731a339842afcbb2643bc93628d4ab9c52d1bf26a7b085ca8f35bba6e/xattr-1.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:937d8c91f6f372788aff8cc0984c4be3f0928584839aaa15ff1c95d64562071c", size = 23474, upload-time = "2025-10-13T22:16:16.33Z" }, { url = "https://files.pythonhosted.org/packages/1b/25/6741ed3d4371eaa2fae70b259d17a580d858ebff8af0042a59e11bb6385f/xattr-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e470b3f15e9c3e263662506ff26e73b3027e1c9beac2cbe9ab89cad9c70c0495", size = 18558, upload-time = "2025-10-13T22:16:17.251Z" }, { url = "https://files.pythonhosted.org/packages/ba/84/cc450688abeb8647aa93a62c1435bb532db11313abfeb9d43b28b4751503/xattr-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f2238b2a973fcbf5fefa1137db97c296d27f4721f7b7243a1fac51514565e9ec", size = 18869, upload-time = "2025-10-13T22:16:18.607Z" }, @@ -8770,34 +9191,73 @@ wheels = [ [[package]] name = "xgrammar" -version = "0.1.33" +version = "0.1.27" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version < '3.13' and sys_platform == 'linux'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version < '3.13' and sys_platform == 'darwin'", +] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra != 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra != 'extra-7-nemo-rl-fsdp' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "mlx-lm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "ninja" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" } }, + { name = "pydantic" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "transformers" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/e1/b522b1e50fddd773d368c2945ef5ed628aa90c0c972027f9aa5a51d6d4f9/xgrammar-0.1.27.tar.gz", hash = "sha256:40af7bb2891f1633ec7f660723c74a92a963307d283aca9e3b4e53a0feaf1d46", size = 2303435, upload-time = "2025-11-04T03:11:53.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/b6/09b43e2adff45d30ebcf9110d0ff753f4c96b368adaa2d166df3dee88d5f/xgrammar-0.1.27-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:6404a7714440eb86ab0379d749f33591274eeef04787dc00d61f22069f3ed51d", size = 663319, upload-time = "2025-11-04T03:11:28.682Z" }, + { url = "https://files.pythonhosted.org/packages/88/8b/53eb5c6d0df8df9f6350f182516a5b8c7b8b11d62650300d2c04af2bc4ea/xgrammar-0.1.27-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d01fa9894bc44a7f6a70b0301b59f3e310c0e0e7b7ea4cf5ce190b12d8220dd8", size = 636168, upload-time = "2025-11-04T03:11:30.373Z" }, + { url = "https://files.pythonhosted.org/packages/08/1b/53d30395bb973f13255d3e3a72961f95fdfb4083877c3f93bb626e3d1522/xgrammar-0.1.27-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:906c0601bac9170e1bab77ca985259035ff9c386c347efcb191555eab86e984e", size = 8676340, upload-time = "2025-11-04T03:11:32.203Z" }, + { url = "https://files.pythonhosted.org/packages/48/74/70cfac0171d9f309cfe18c5384330e3edc9466c436b258495fd30ecf29a3/xgrammar-0.1.27-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb68988a122f544301c496f2cac8ee82960ca7f5b3a42a952b2a00c0a55e6ca5", size = 8870650, upload-time = "2025-11-04T03:11:34.322Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/0392aa9c7669c56f7f88e4423b246476a74a72c3bb9db944e1bfc029985e/xgrammar-0.1.27-cp312-cp312-win_amd64.whl", hash = "sha256:3aac335ea052afc8f8dc34b9f2afcb9462a68189423aed9f60b0941db6cfc310", size = 708811, upload-time = "2025-11-04T03:11:36.214Z" }, + { url = "https://files.pythonhosted.org/packages/a4/77/5aee819c00844fb333fa802507182aa19445b347840103a14bd27ed944c4/xgrammar-0.1.27-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e248488c7c8a8ba175c7d1c5b55a2dd705661bbaa87755a749f9fdda146cbe1e", size = 636084, upload-time = "2025-11-04T03:11:38.192Z" }, + { url = "https://files.pythonhosted.org/packages/23/c2/cd15c44bd6db4411fc733303e0b85033772f3389b32210e6f0ae08f5a2c1/xgrammar-0.1.27-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac7a307d7a739962c422969cb486aa3994e200bfa6191d9519fdca5224760f0", size = 8870005, upload-time = "2025-11-04T03:11:40.039Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/d3d3dc97c05159d9336fb4b947b22bd074ca259bd291be523c00e5696d24/xgrammar-0.1.27-cp313-cp313-win_amd64.whl", hash = "sha256:37936e04974bcb4c02a69ab734ff530669a43b03b2910c4013233dd074896ac9", size = 708726, upload-time = "2025-11-04T03:11:42.064Z" }, +] + +[[package]] +name = "xgrammar" +version = "0.1.29" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version < '3.13' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "mlx-lm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, { name = "pydantic" }, - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, - { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-automodel' and extra != 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm')" }, + { name = "torch", version = "2.10.0+cu129", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(sys_platform != 'darwin' and extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-vllm') or (sys_platform != 'darwin' and extra != 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-fsdp') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-mcore') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-automodel' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-fsdp' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-sglang') or (extra == 'extra-7-nemo-rl-mcore' and extra == 'extra-7-nemo-rl-vllm') or (extra == 'extra-7-nemo-rl-sglang' and extra == 'extra-7-nemo-rl-vllm')" }, { name = "transformers" }, - { name = "triton", version = "3.6.0", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/43/e5dfddb1d2a4fccf3e3a88f103e88698cdefc3182f4e169a359ffe1c1794/xgrammar-0.1.33.tar.gz", hash = "sha256:8dbe5fc3d76651ab1fac7a68fc2a118b885fa0ec7189927fb6e0dce0081aea99", size = 2398956, upload-time = "2026-03-27T10:16:36.582Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/a3/70dbe3ffd331a1e7e1ad5a95690a4086e6c7cdb8089f5c7eda712219ccec/xgrammar-0.1.29.tar.gz", hash = "sha256:cf195afa81b489eebf35d4c6f37f27136d05420739ab4a6f7f065c938d7e4baa", size = 2321317, upload-time = "2025-12-19T08:23:54.53Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/b1/cce9f6d12b9de0db8b86401ea739fe79ac555f3da56e47faa5b874d41e42/xgrammar-0.1.33-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e5b46b922fb04fd1848198da5273ddc20f16693fba5871bac1837f1c90f59584", size = 22702353, upload-time = "2026-03-27T10:15:27.203Z" }, - { url = "https://files.pythonhosted.org/packages/6b/55/4d186d4065f645a051be992919c51aaf96cfa8a32f7ecc8512a6e41f969f/xgrammar-0.1.33-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7eec984a20fd54d4c79536d99e2515bac54bd4e1380162fa047f5ff45bdf6d8", size = 42133430, upload-time = "2026-03-27T10:15:31.409Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ca/db765035b3bb1854bdb833c118e0f09dacc623ce5e867466d63610d635fa/xgrammar-0.1.33-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d705f62d91a3675997a81d09aa371c375d7793ce1021aff7b7ed5a92021c7379", size = 42206830, upload-time = "2026-03-27T10:15:35.574Z" }, - { url = "https://files.pythonhosted.org/packages/f5/17/635fc8933b35f24d0749fe177209abb5b526c99a2d098abb71c0e601f356/xgrammar-0.1.33-cp313-cp313-win_amd64.whl", hash = "sha256:2c626de8f503858efa28cab099cbb1719c4926af4250e8dea8efddfa2c6b6c91", size = 7222102, upload-time = "2026-03-27T10:15:38.617Z" }, - { url = "https://files.pythonhosted.org/packages/0e/fd/ec456e86c2d3f1c01addac8123765ee3b77c1aa1c62298f0f0fdf57d5499/xgrammar-0.1.33-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:59b9c3bb8cc0c5c82155cdc4ff0fd9f7b82348346d9e11307d5df2d3cc81eb3b", size = 22765922, upload-time = "2026-03-27T10:15:41.617Z" }, - { url = "https://files.pythonhosted.org/packages/ac/24/8afe2a6dd42f1d1ac2f702c9fdc757e724f4af4c6eae5508e063acfb70a9/xgrammar-0.1.33-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a55912a1d7dcd3d2134617083c28cffe97ba4eaafff39bd41ca28a5ffda66073", size = 22702579, upload-time = "2026-03-27T10:15:45.14Z" }, - { url = "https://files.pythonhosted.org/packages/b0/79/8fbd675aa49b180d0912aeb90fa72dca9bb1f476724f76d3097561cca161/xgrammar-0.1.33-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bba458ffe06b3774be3a24eaf58dc217eec3a781ba41340c2eecf76aa9347aa3", size = 42133038, upload-time = "2026-03-27T10:15:48.98Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c5/64558fd11130624267f788be5d665f898f627b87c6916b523c6e0d4cebf9/xgrammar-0.1.33-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:776a15eaadda463987fba97d8a07b60c262c96353d800fc8639efedb57b7cbbb", size = 42206382, upload-time = "2026-03-27T10:15:53.458Z" }, - { url = "https://files.pythonhosted.org/packages/9c/37/921445f60d8e8cba7caa006d7583e8a059b07e6d7eb5c9dc150c2219415a/xgrammar-0.1.33-cp314-cp314-win_amd64.whl", hash = "sha256:247dec78e11f5c361f7f3f2bc571574c118fc88045bf13fc792b6432fc32bc68", size = 7312992, upload-time = "2026-03-27T10:15:56.274Z" }, - { url = "https://files.pythonhosted.org/packages/a2/31/fb51cd12733e53c3832f6cfadf5f958414102c80c477beb8b233bd4324f4/xgrammar-0.1.33-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:915451b9c86c840e44500782696650ef5535eab87662d748d649f0569d84c322", size = 22768311, upload-time = "2026-03-27T10:15:59.231Z" }, - { url = "https://files.pythonhosted.org/packages/6e/1e/f3de2066ac889a8b4f4589b617364a07a46aea40097c8e0abd62b61f72a4/xgrammar-0.1.33-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c98e2c89bb5efa31e5f3648159dc3477dde51bbf3da8138d6b6fdc49c8fddd44", size = 22705773, upload-time = "2026-03-27T10:16:02.918Z" }, - { url = "https://files.pythonhosted.org/packages/70/fb/523113e066b74428b843e66baed815671faa1dd366a2967b687498aa8cba/xgrammar-0.1.33-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51e52aef50c2d91122a23ce67f7b187fc6caffa620b7412fd3a5eebb00a29377", size = 42134611, upload-time = "2026-03-27T10:16:07.459Z" }, - { url = "https://files.pythonhosted.org/packages/c5/07/6ea6bf8efff3c29c07f594f1e8665dc3ed43abdad86a6a27da9a3ddcbbef/xgrammar-0.1.33-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cbb91580da8ac6c86de0464339c1ca1899fb0032d604175bdb384c1a13b9b7", size = 42197758, upload-time = "2026-03-27T10:16:12.504Z" }, - { url = "https://files.pythonhosted.org/packages/e3/05/813842e384723c636ad61b6902117dd689d8c04a8d34e0da91b35fbb9f8b/xgrammar-0.1.33-cp314-cp314t-win_amd64.whl", hash = "sha256:ae8877dc35cbdf07b23ce5584e093cca36ed72ad35798d4ce6ed858cd3b19fea", size = 7317563, upload-time = "2026-03-27T10:16:15.774Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d8/fb282fc78be6e9bbefb5cb389f66b22e4efd6ae14f06234f599651620da5/xgrammar-0.1.29-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:d992a3cee7594bbdaa64ae59f90da5ce21c5fe654719df3816014289ada6f04d", size = 16007376, upload-time = "2025-12-19T08:23:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/82/a7/2c9767620ee50f2f40f1eb95e55a3a29e1a0670f087ee6dc1bc1c887b906/xgrammar-0.1.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1bbdf02e45cfa8614218ba01ca7952d375f8bc1c13884e3d04daa4b54180cbc2", size = 17913535, upload-time = "2025-12-19T08:23:26.02Z" }, + { url = "https://files.pythonhosted.org/packages/57/94/18793c64bf0368075a34c06e196bf002f1e6ab0aee332268f44e8d356d5a/xgrammar-0.1.29-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6eb370a16b27a683e5f2b9e429ab41440c69977d4a504849ed61831b94cc704c", size = 34705239, upload-time = "2025-12-19T08:23:28.369Z" }, + { url = "https://files.pythonhosted.org/packages/3e/da/4c14e3e00be698009b52700f15326a23272b4b00475939b6acc86b151188/xgrammar-0.1.29-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79e6e4f5cd33be77418cf91efc482f2b3d773d309891224383bc8a4948ad7b07", size = 34906135, upload-time = "2025-12-19T08:23:30.838Z" }, + { url = "https://files.pythonhosted.org/packages/22/d8/34423997f48627cef3b74cc894d9dfcaacae02941c06237ac5f3196406a7/xgrammar-0.1.29-cp312-cp312-win_amd64.whl", hash = "sha256:39bdfadedbce34599835486164fa80ba00248c6c75ad91f3843db90ef37e037f", size = 5928381, upload-time = "2025-12-19T08:23:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ef/8a4b4cb10fc996c0a25c9bf5613aaf5a86114291a9a4003e43605cab42bf/xgrammar-0.1.29-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fedf21e447ef646f23a6e2d11877c0812d55965dcf8c0aa9b0f32590c9b6e22a", size = 17913609, upload-time = "2025-12-19T08:23:36.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/c5/e4965c9921e7bb6061f246ae7f8c7b9b1dfc21262248100c2f9b398b361e/xgrammar-0.1.29-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb22aea775971f7d8c4d0e193257ebeb71b68acd9d36af3331ca5fd4d9a46991", size = 34904126, upload-time = "2025-12-19T08:23:38.335Z" }, + { url = "https://files.pythonhosted.org/packages/09/26/641d7ee1a59e526aa94be980c485f899088d09dd1af517a2e1d0e85853bc/xgrammar-0.1.29-cp313-cp313-win_amd64.whl", hash = "sha256:12e6d63e892e9da8d088569dd629af58a5eafd909dc58788d499c4fd74bcd2a1", size = 5928450, upload-time = "2025-12-19T08:23:40.667Z" }, ] [[package]] @@ -8806,6 +9266,21 @@ version = "3.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/00/5e/d6e5258d69df8b4ed8c83b6664f2b47d30d2dec551a29ad72a6c69eafd31/xxhash-3.5.0.tar.gz", hash = "sha256:84f2caddf951c9cbf8dc2e22a89d4ccf5d86391ac6418fe81e3c67d0cf60b45f", size = 84241, upload-time = "2024-08-17T09:20:38.972Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/07/0e/1bfce2502c57d7e2e787600b31c83535af83746885aa1a5f153d8c8059d6/xxhash-3.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:14470ace8bd3b5d51318782cd94e6f94431974f16cb3b8dc15d52f3b69df8e00", size = 31969, upload-time = "2024-08-17T09:18:24.025Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d6/8ca450d6fe5b71ce521b4e5db69622383d039e2b253e9b2f24f93265b52c/xxhash-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59aa1203de1cb96dbeab595ded0ad0c0056bb2245ae11fac11c0ceea861382b9", size = 30787, upload-time = "2024-08-17T09:18:25.318Z" }, + { url = "https://files.pythonhosted.org/packages/5b/84/de7c89bc6ef63d750159086a6ada6416cc4349eab23f76ab870407178b93/xxhash-3.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08424f6648526076e28fae6ea2806c0a7d504b9ef05ae61d196d571e5c879c84", size = 220959, upload-time = "2024-08-17T09:18:26.518Z" }, + { url = "https://files.pythonhosted.org/packages/fe/86/51258d3e8a8545ff26468c977101964c14d56a8a37f5835bc0082426c672/xxhash-3.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61a1ff00674879725b194695e17f23d3248998b843eb5e933007ca743310f793", size = 200006, upload-time = "2024-08-17T09:18:27.905Z" }, + { url = "https://files.pythonhosted.org/packages/02/0a/96973bd325412feccf23cf3680fd2246aebf4b789122f938d5557c54a6b2/xxhash-3.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f2c61bee5844d41c3eb015ac652a0229e901074951ae48581d58bfb2ba01be", size = 428326, upload-time = "2024-08-17T09:18:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/11/a7/81dba5010f7e733de88af9555725146fc133be97ce36533867f4c7e75066/xxhash-3.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d32a592cac88d18cc09a89172e1c32d7f2a6e516c3dfde1b9adb90ab5df54a6", size = 194380, upload-time = "2024-08-17T09:18:30.706Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7d/f29006ab398a173f4501c0e4977ba288f1c621d878ec217b4ff516810c04/xxhash-3.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70dabf941dede727cca579e8c205e61121afc9b28516752fd65724be1355cc90", size = 207934, upload-time = "2024-08-17T09:18:32.133Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6e/6e88b8f24612510e73d4d70d9b0c7dff62a2e78451b9f0d042a5462c8d03/xxhash-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e5d0ddaca65ecca9c10dcf01730165fd858533d0be84c75c327487c37a906a27", size = 216301, upload-time = "2024-08-17T09:18:33.474Z" }, + { url = "https://files.pythonhosted.org/packages/af/51/7862f4fa4b75a25c3b4163c8a873f070532fe5f2d3f9b3fc869c8337a398/xxhash-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e5b5e16c5a480fe5f59f56c30abdeba09ffd75da8d13f6b9b6fd224d0b4d0a2", size = 203351, upload-time = "2024-08-17T09:18:34.889Z" }, + { url = "https://files.pythonhosted.org/packages/22/61/8d6a40f288f791cf79ed5bb113159abf0c81d6efb86e734334f698eb4c59/xxhash-3.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149b7914451eb154b3dfaa721315117ea1dac2cc55a01bfbd4df7c68c5dd683d", size = 210294, upload-time = "2024-08-17T09:18:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/17/02/215c4698955762d45a8158117190261b2dbefe9ae7e5b906768c09d8bc74/xxhash-3.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:eade977f5c96c677035ff39c56ac74d851b1cca7d607ab3d8f23c6b859379cab", size = 414674, upload-time = "2024-08-17T09:18:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/b7a8db8a3237cff3d535261325d95de509f6a8ae439a5a7a4ffcff478189/xxhash-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa9f547bd98f5553d03160967866a71056a60960be00356a15ecc44efb40ba8e", size = 192022, upload-time = "2024-08-17T09:18:40.138Z" }, + { url = "https://files.pythonhosted.org/packages/78/e3/dd76659b2811b3fd06892a8beb850e1996b63e9235af5a86ea348f053e9e/xxhash-3.5.0-cp312-cp312-win32.whl", hash = "sha256:f7b58d1fd3551b8c80a971199543379be1cee3d0d409e1f6d8b01c1a2eebf1f8", size = 30170, upload-time = "2024-08-17T09:18:42.163Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6b/1c443fe6cfeb4ad1dcf231cdec96eb94fb43d6498b4469ed8b51f8b59a37/xxhash-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:fa0cafd3a2af231b4e113fba24a65d7922af91aeb23774a8b78228e6cd785e3e", size = 30040, upload-time = "2024-08-17T09:18:43.699Z" }, + { url = "https://files.pythonhosted.org/packages/0f/eb/04405305f290173acc0350eba6d2f1a794b57925df0398861a20fbafa415/xxhash-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:586886c7e89cb9828bcd8a5686b12e161368e0064d040e225e72607b43858ba2", size = 26796, upload-time = "2024-08-17T09:18:45.29Z" }, { url = "https://files.pythonhosted.org/packages/c9/b8/e4b3ad92d249be5c83fa72916c9091b0965cb0faeff05d9a0a3870ae6bff/xxhash-3.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37889a0d13b0b7d739cfc128b1c902f04e32de17b33d74b637ad42f1c55101f6", size = 31795, upload-time = "2024-08-17T09:18:46.813Z" }, { url = "https://files.pythonhosted.org/packages/fc/d8/b3627a0aebfbfa4c12a41e22af3742cf08c8ea84f5cc3367b5de2d039cce/xxhash-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97a662338797c660178e682f3bc180277b9569a59abfb5925e8620fba00b9fc5", size = 30792, upload-time = "2024-08-17T09:18:47.862Z" }, { url = "https://files.pythonhosted.org/packages/c3/cc/762312960691da989c7cd0545cb120ba2a4148741c6ba458aa723c00a3f8/xxhash-3.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f85e0108d51092bdda90672476c7d909c04ada6923c14ff9d913c4f7dc8a3bc", size = 220950, upload-time = "2024-08-17T09:18:49.06Z" }, @@ -8829,6 +9304,13 @@ version = "1.6.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/02/5b/cfde09baf28f7046194b98f1c4907e172c48e7c1b2db35a918fc8a57727a/yappi-1.6.10.tar.gz", hash = "sha256:463b822727658937bd95a7d80ca9758605b8cd0014e004e9e520ec9cb4db0c92", size = 59379, upload-time = "2024-11-12T11:24:38.351Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/50/01/1823649d33aee627440939d7247e1fa7ef64bd907ca4ea88438274d392fc/yappi-1.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32c6d928604d7a236090bc36d324f309fe8344c91123bb84e37c43f6677adddc", size = 32914, upload-time = "2024-11-12T11:23:45.877Z" }, + { url = "https://files.pythonhosted.org/packages/54/c5/85852db160c93ee3190741a4fff25075518ad97dea1e2ad47ca6eab31d2f/yappi-1.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9683c40de7e4ddff225068032cd97a6d928e4beddd9c7cf6515325be8ac28036", size = 77223, upload-time = "2024-11-12T11:23:46.834Z" }, + { url = "https://files.pythonhosted.org/packages/38/01/b03a2bc47fbb2d9bcad072fc2e08730f814defaac2ffbf76ef785fdff5d0/yappi-1.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:733a212014f2b44673ed62be53b3d4dd458844cd2008ba107f27a3293e42f43a", size = 81250, upload-time = "2024-11-12T11:23:47.872Z" }, + { url = "https://files.pythonhosted.org/packages/39/44/a3c64e0de45a0fc0bf327af95465a94cb8340a64e5abb7bb8af1cfd76f7f/yappi-1.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7d80938e566ac6329daa3b036fdf7bd34488010efcf0a65169a44603878daa4e", size = 76118, upload-time = "2024-11-12T11:23:48.829Z" }, + { url = "https://files.pythonhosted.org/packages/0c/68/6806060eaec421a21554c2f7ee8b1379ff02b059e0c753eb55e5b7b701a4/yappi-1.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:01705971b728a4f95829b723d08883c7623ec275f4066f4048b28dc0151fe0af", size = 78522, upload-time = "2024-11-12T11:23:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/af/4f/0afcacc683f3c34570effc78e6d4c154dea9d6cc8549c2535fb75441be30/yappi-1.6.10-cp312-cp312-win32.whl", hash = "sha256:8dd13a430b046e2921ddf63d992da97968724b41a03e68292f06a2afa11c9d6e", size = 32020, upload-time = "2024-11-12T11:23:51.51Z" }, + { url = "https://files.pythonhosted.org/packages/cb/45/17f50baed4a886fab2c34a040cefefe6623abcaaadf23f851207da9cd5e6/yappi-1.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:a50eb3aec893c40554f8f811d3341af266d844e7759f7f7abfcdba2744885ea3", size = 34471, upload-time = "2024-11-12T11:23:52.457Z" }, { url = "https://files.pythonhosted.org/packages/2c/33/9ca066f48c7fb21e0ab16fd5e1c99771275a8cec435ef7ac1840d13252f0/yappi-1.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:944df9ebc6b283d6591a6b5f4c586d0eb9c6131c915f1b20fb36127ade83720d", size = 32924, upload-time = "2024-11-12T11:23:53.435Z" }, { url = "https://files.pythonhosted.org/packages/cc/ef/a81fac59ca7a13fd26321d59a54841f70f76ce91b5884c001d77f534b3b1/yappi-1.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3736ea6458edbabd96918d88e2963594823e4ab4c58d62a52ef81f6b5839ec19", size = 77308, upload-time = "2024-11-12T11:23:55.393Z" }, { url = "https://files.pythonhosted.org/packages/62/59/8fdcb2a660388a7778c52cdfa0c52654955cf7953f85efacd8fd771f8da0/yappi-1.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f27bbc3311a3662231cff395d38061683fac5c538f3bab6796ff05511d2cce43", size = 81347, upload-time = "2024-11-12T11:23:56.926Z" }, @@ -8849,6 +9331,23 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload-time = "2025-06-10T00:43:44.369Z" }, + { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload-time = "2025-06-10T00:43:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload-time = "2025-06-10T00:43:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" }, + { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload-time = "2025-06-10T00:43:59.773Z" }, + { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload-time = "2025-06-10T00:44:06.527Z" }, + { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" }, + { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload-time = "2025-06-10T00:44:14.731Z" }, + { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" }, { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, @@ -8894,43 +9393,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50e wheels = [ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] - -[[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/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, - { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, - { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, - { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, - { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, - { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, - { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, - { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, - { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, - { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, - { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, - { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, - { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, - { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, - { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, - { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, - { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, - { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, - { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, - { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, - { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, - { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, - { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, -] From 26ee36ddea2f80afdc6d5a1ab5c57dde8254cc0d Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 19 Apr 2026 23:12:57 -0700 Subject: [PATCH 78/84] infra: revert Gym submodule bump The standalone_gym_server has been moved to nemo_rl/distributed/ so the Gym submodule change from tk/standalone-server is no longer needed. The standalone server could potentially be upstreamed to Gym in the future, but for now it lives in nemo-rl since it's highly in flux. Signed-off-by: Terry Kong --- 3rdparty/Gym-workspace/Gym | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty/Gym-workspace/Gym b/3rdparty/Gym-workspace/Gym index 01a9765f8cb..1a4912e231b 160000 --- a/3rdparty/Gym-workspace/Gym +++ b/3rdparty/Gym-workspace/Gym @@ -1 +1 @@ -Subproject commit 01a9765f8cba758c12a018b0e0e4d861ee4b916c +Subproject commit 1a4912e231bb2795b062f7de97496caaf382c7f6 From b7d79e14857db321817291b56ba478889c59649d Mon Sep 17 00:00:00 2001 From: Hemil Desai Date: Thu, 16 Apr 2026 19:25:22 -0700 Subject: [PATCH 79/84] =?UTF-8?q?feat:=20disaggregated=20generation=20?= =?UTF-8?q?=E2=80=94=20JSON-only=20transport=20+=20/reset=5Fcollective?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplifies the cross-cluster disagg path to a single transport (OpenAI /v1/completions per DP shard) and adds an idempotent teardown endpoint so the generation cluster can accept a new training cluster's NCCL weight-sync group without a pod restart. Transport simplification (drops ~225 LoC of unused branching): * Remove the "head" tensor /generate endpoint on the control server. * Remove the per-shard /generate endpoint on each vLLM worker. * Remove _send_generate_to_head, _generate_direct_to_shards, _fast_serialize, _fast_deserialize, _shard_generate_urls, _get_session, _aio_session from RemoteGeneration; collapse generate()/generate_async() to the json path. * Drop disagg_mode and disagg_generation_routing config keys. Disaggregated HTTP mode is now implicit when remote_generation_url is set and colocated.enabled=false. Add /reset_collective so the generation server can tear down an orphaned NCCL group (e.g. after a training restart) without losing the running vLLM engines. Plumbed through VllmGeneration -> VllmGenerationWorker( Sync/Async) -> VllmInternalWorkerExtension -> StatelessProcessGroup.destroy, which aborts the NCCL communicator and drops the TCP store. Also adds unit coverage for the surviving json path: response parsing for token_id:NNN logprobs with None substitution, round-robin shard selection across calls, and greedy sampling overrides. Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Hemil Desai Signed-off-by: Terry Kong --- nemo_rl/algorithms/grpo.py | 86 +++- .../distributed/stateless_process_group.py | 25 ++ .../generation/generation_control_server.py | 346 +++++++++++++++ nemo_rl/models/generation/interfaces.py | 5 + .../models/generation/remote_generation.py | 414 ++++++++++++++++++ .../models/generation/vllm/vllm_backend.py | 18 +- .../models/generation/vllm/vllm_generation.py | 17 + nemo_rl/models/generation/vllm/vllm_worker.py | 3 + .../generation/vllm/vllm_worker_async.py | 51 ++- .../generation/test_remote_generation_http.py | 203 +++++++++ 10 files changed, 1144 insertions(+), 24 deletions(-) create mode 100644 nemo_rl/models/generation/generation_control_server.py create mode 100644 nemo_rl/models/generation/remote_generation.py create mode 100644 tests/unit/models/generation/test_remote_generation_http.py diff --git a/nemo_rl/algorithms/grpo.py b/nemo_rl/algorithms/grpo.py index da955ac32a3..3ab21b23fd2 100644 --- a/nemo_rl/algorithms/grpo.py +++ b/nemo_rl/algorithms/grpo.py @@ -394,6 +394,12 @@ def init_train_dataloader(dataset, suffix: str = ""): # ========================== print("\n▶ Setting up compute cluster...", flush=True) colocated_inference = generation_config["colocated"]["enabled"] + # HTTP-only disagg: gen server is external (separate cluster/process). + # Implicitly enabled when not colocated and `remote_generation_url` is set. + disagg_http_mode = ( + not colocated_inference + and generation_config.get("remote_generation_url") is not None + ) env_name_list = extract_necessary_env_names(data_config) rm_env_enabled = "reward_model" in env_name_list @@ -457,8 +463,17 @@ def init_train_dataloader(dataset, suffix: str = ""): inference_gpus_per_node = inference_resources["gpus_per_node"] inference_nodes = inference_resources["num_nodes"] - # validate and configure resources - if policy_nodes == 1: + if disagg_http_mode: + # External gen server manages its own GPUs. Training cluster uses + # ALL gpus from cluster config — no subtraction. + inference_nodes = inference_nodes or 1 + print( + f" ⚡ Disagg HTTP mode: training uses {train_gpus_per_node} GPUs/node, " + f"inference is external ({inference_gpus_per_node}×{inference_nodes} GPUs)", + flush=True, + ) + elif policy_nodes == 1: + # validate and configure resources # When policy_nodes == 1, train and inference are on the same node assert ( inference_gpus_per_node is not None and inference_gpus_per_node > 0 @@ -518,18 +533,26 @@ def init_train_dataloader(dataset, suffix: str = ""): flush=True, ) - # initialize inference cluster - inference_cluster = RayVirtualCluster( - name="grpo_inference_cluster", - bundle_ct_per_node_list=[inference_gpus_per_node] * inference_nodes, - use_gpus=True, - num_gpus_per_node=inference_gpus_per_node, - max_colocated_worker_groups=1, - ) - print( - f" ✓ Ray inference cluster initialized with {inference_nodes} nodes with {inference_gpus_per_node} GPUs per node", - flush=True, - ) + if disagg_http_mode: + # No local inference cluster — gen server is external + inference_cluster = None + print( + " ✓ No local inference cluster (disagg HTTP mode)", + flush=True, + ) + else: + # initialize inference cluster + inference_cluster = RayVirtualCluster( + name="grpo_inference_cluster", + bundle_ct_per_node_list=[inference_gpus_per_node] * inference_nodes, + use_gpus=True, + num_gpus_per_node=inference_gpus_per_node, + max_colocated_worker_groups=1, + ) + print( + f" ✓ Ray inference cluster initialized with {inference_nodes} nodes with {inference_gpus_per_node} GPUs per node", + flush=True, + ) # ========================== # Training and Inference @@ -684,13 +707,34 @@ def initialize_generation_with_policy( "hf_config_overrides", {} ) - policy_generation, policy = initialize_generation_with_policy( - init_generation_fn=init_vllm, - generation_name="vLLM", - init_time_key="vllm_init_time_s", - colocated_inference=colocated_inference, - worker_init_timing_metrics=worker_init_timing_metrics, - ) + if disagg_http_mode: + # HTTP-only disagg: gen server is external, don't create VllmGeneration. + # Only initialize policy training workers. + from nemo_rl.models.generation.remote_generation import RemoteGeneration + + remote_url = generation_config["remote_generation_url"] + assert remote_url, ( + "remote_generation_url must be set for disaggregated HTTP mode" + ) + + policy_generation = RemoteGeneration( + generation=None, server_url=remote_url, config=generation_config + ) + print( + f" ✓ Using HTTP-only RemoteGeneration (URL: {remote_url})", + flush=True, + ) + + policy, policy_time = init_policy() + worker_init_timing_metrics["policy_init_time_s"] = policy_time + else: + policy_generation, policy = initialize_generation_with_policy( + init_generation_fn=init_vllm, + generation_name="vLLM", + init_time_key="vllm_init_time_s", + colocated_inference=colocated_inference, + worker_init_timing_metrics=worker_init_timing_metrics, + ) print( f" ✓ Using vLLM backend for generation with {policy_config['model_name']}", diff --git a/nemo_rl/distributed/stateless_process_group.py b/nemo_rl/distributed/stateless_process_group.py index b7fd5012854..3f15c0c12c3 100644 --- a/nemo_rl/distributed/stateless_process_group.py +++ b/nemo_rl/distributed/stateless_process_group.py @@ -70,3 +70,28 @@ def broadcast( self.nccl_communicator.broadcast( sendbuf=tensor, recvbuf=tensor, root=src, stream=int(stream.cuda_stream) ) + + def destroy(self): + """Tear down the NCCL communicator and TCP store so the group can be re-initialized. + + Safe to call even if the communicator was never initialized. Any errors raised by + the NCCL library (e.g. the peers have already gone away) are swallowed so the + caller can always bring the group back up with a fresh `init_nccl_communicator`. + """ + comm = getattr(self, "nccl_communicator", None) + if comm is not None: + for method in ("abort", "destroy", "finalize"): + fn = getattr(comm, method, None) + if callable(fn): + try: + fn() + break + except Exception: + continue + self.nccl_communicator = None # type: ignore[assignment] + store = getattr(self, "tcp_store", None) + if store is not None: + try: + del self.tcp_store + except Exception: + pass diff --git a/nemo_rl/models/generation/generation_control_server.py b/nemo_rl/models/generation/generation_control_server.py new file mode 100644 index 00000000000..3816ee6b9f5 --- /dev/null +++ b/nemo_rl/models/generation/generation_control_server.py @@ -0,0 +1,346 @@ +# 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. +"""Control plane server + DP shard router for disaggregated vLLM generation. + +Wraps a local VllmGeneration instance and exposes: + A) Control plane — weight sync triggers, lifecycle management, collective init. + B) DP Shard Router — reverse proxy for vLLM's OpenAI-compatible HTTP API with + intelligent routing across data-parallel shards. + +Runs on a single port (default 8089). Training cluster discovers this one URL. +The training client sends generation requests directly to per-shard +``/v1/completions`` endpoints (round-robin); the reverse proxy here is for +external OpenAI-compatible clients (e.g. NemoGym). +""" + +from __future__ import annotations + +import asyncio +import hashlib +import io +import json +import threading +import traceback +from enum import Enum +from typing import Any, Optional + +import aiohttp +import ray +import torch +from fastapi import FastAPI, Request, Response +from fastapi.responses import JSONResponse, StreamingResponse + +from nemo_rl.models.generation.interfaces import GenerationInterface + + +class RoutingStrategy(str, Enum): + ROUND_ROBIN = "round_robin" + LEAST_PENDING = "least_pending" + PREFIX_HASH = "prefix_hash" + + +class GenerationControlServer: + """FastAPI server wrapping a VllmGeneration for disaggregated mode.""" + + def __init__( + self, + generation: GenerationInterface, + port: int = 8089, + routing_strategy: str = "round_robin", + ): + self.generation = generation + self.port = port + self.routing_strategy = RoutingStrategy(routing_strategy) + + self.shard_urls: list[str] = [ + url + for url in getattr(generation, "dp_openai_server_base_urls", []) + if url is not None + ] + + self._rr_index = 0 + self._pending_counts: dict[int, int] = {i: 0 for i in range(len(self.shard_urls))} + self._lock = asyncio.Lock() + self._session: Optional[aiohttp.ClientSession] = None + + self._app = self._build_app() + self._server_thread: Optional[threading.Thread] = None + + async def _get_session(self) -> aiohttp.ClientSession: + """Lazily create a persistent aiohttp session for the router.""" + if self._session is None or self._session.closed: + self._session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=600) + ) + return self._session + + def _build_app(self): + app = FastAPI(title="Generation Control Server") + + # ===================================================================== + # Control plane endpoints + # ===================================================================== + + @app.get("/health") + async def health(): + return {"status": "ok"} + + @app.get("/config") + async def get_config(): + return dict(self.generation.cfg) + + @app.get("/dp_openai_server_base_urls") + async def get_dp_urls(): + return self.shard_urls + + # All control plane endpoints use run_in_executor to avoid blocking + # the uvicorn event loop. Blocking ray.get() or synchronous GPU operations + # in async handlers deadlocks the NCCL warmup broadcast. + async def _run_blocking(fn, *args): + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, fn, *args) + + @app.post("/init_collective") + async def init_collective(request: Request): + body = await request.json() + try: + def _do(): + futures = self.generation.init_collective( + ip=body["ip"], + port=body["port"], + world_size=body["world_size"], + train_world_size=body["train_world_size"], + ) + ray.get(futures) + await _run_blocking(_do) + return {"success": True} + except Exception as e: + traceback.print_exc() + return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + + @app.post("/reset_collective") + async def reset_collective(): + """Tear down the weight-sync NCCL group so a new training run can re-init. + + Idempotent — safe to call when no collective is currently held. + """ + try: + def _do(): + futures = self.generation.reset_collective() + if futures: + ray.get(futures) + await _run_blocking(_do) + return {"success": True} + except Exception as e: + traceback.print_exc() + return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + + @app.post("/update_weights_from_collective") + async def update_weights_from_collective(): + try: + def _do(): + futures = self.generation.update_weights_from_collective() + results = ray.get(futures) + success = all(r for r in results if r is not None) + if not success: + raise RuntimeError( + f"One or more workers failed to update weights. Results: {results}" + ) + return success + success = await _run_blocking(_do) + return {"success": success} + except Exception as e: + traceback.print_exc() + return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + + @app.post("/prepare_for_generation") + async def prepare_for_generation(): + try: + result = await _run_blocking(self.generation.prepare_for_generation) + return {"success": bool(result)} + except Exception as e: + traceback.print_exc() + return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + + @app.post("/finish_generation") + async def finish_generation(): + try: + result = await _run_blocking(self.generation.finish_generation) + return {"success": bool(result)} + except Exception as e: + traceback.print_exc() + return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + + @app.post("/prepare_refit_info") + async def prepare_refit_info(request: Request): + try: + body_bytes = await request.body() + def _do(): + state_dict_info = torch.load(io.BytesIO(body_bytes), weights_only=False) + self.generation.prepare_refit_info(state_dict_info) + await _run_blocking(_do) + return {"success": True} + except Exception as e: + traceback.print_exc() + return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + + @app.post("/invalidate_kv_cache") + async def invalidate_kv_cache(): + try: + result = await _run_blocking(self.generation.invalidate_kv_cache) + return {"success": bool(result)} + except Exception as e: + traceback.print_exc() + return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + + @app.post("/clear_logger_metrics") + async def clear_logger_metrics(): + self.generation.clear_logger_metrics() + return {"success": True} + + @app.get("/get_logger_metrics") + async def get_logger_metrics(): + return self.generation.get_logger_metrics() + + @app.post("/snapshot_step_metrics") + async def snapshot_step_metrics(): + if hasattr(self.generation, "snapshot_step_metrics"): + self.generation.snapshot_step_metrics() + return {"success": True} + + @app.get("/get_step_metrics") + async def get_step_metrics(): + if hasattr(self.generation, "get_step_metrics"): + return self.generation.get_step_metrics() + return {} + + # ===================================================================== + # DP Shard Router (OpenAI-compatible reverse proxy for external clients) + # ===================================================================== + + @app.post("/v1/completions") + async def route_completions(request: Request): + return await self._route_request(request, "/v1/completions") + + @app.post("/v1/chat/completions") + async def route_chat_completions(request: Request): + return await self._route_request(request, "/v1/chat/completions") + + @app.post("/tokenize") + async def route_tokenize(request: Request): + return await self._route_request(request, "/tokenize") + + return app + + # ===================================================================== + # Router implementation + # ===================================================================== + + def _select_shard(self, body: dict | None = None) -> int: + """Select a DP shard index based on the routing strategy. + + Safe without a lock: FastAPI runs on a single-threaded asyncio loop, + so _rr_index and _pending_counts mutations are non-concurrent. + """ + if len(self.shard_urls) <= 1: + return 0 + + if self.routing_strategy == RoutingStrategy.ROUND_ROBIN: + idx = self._rr_index + self._rr_index = (self._rr_index + 1) % len(self.shard_urls) + return idx + + if self.routing_strategy == RoutingStrategy.LEAST_PENDING: + return min(self._pending_counts, key=self._pending_counts.get) + + if self.routing_strategy == RoutingStrategy.PREFIX_HASH: + prompt_ids = body.get("prompt_token_ids") if body else None + if prompt_ids: + prefix = tuple(prompt_ids[:128]) + h = int(hashlib.md5(str(prefix).encode()).hexdigest(), 16) + return h % len(self.shard_urls) + return min(self._pending_counts, key=self._pending_counts.get) + + return 0 + + async def _route_request(self, request: Request, path: str): + """Forward an HTTP request to a selected DP shard.""" + body_bytes = await request.body() + + body_dict = None + try: + body_dict = json.loads(body_bytes) + except Exception: + pass + + shard_idx = self._select_shard(body_dict) + shard_base_url = self.shard_urls[shard_idx] + base = shard_base_url.rstrip("/") + if base.endswith("/v1"): + base = base[:-3] + target_url = f"{base}{path}" + + self._pending_counts[shard_idx] = self._pending_counts.get(shard_idx, 0) + 1 + try: + is_streaming = body_dict and body_dict.get("stream", False) + session = await self._get_session() + + async with session.post( + target_url, + data=body_bytes, + headers={"Content-Type": request.headers.get("content-type", "application/json")}, + ) as resp: + if is_streaming: + async def stream_response(): + async for chunk in resp.content.iter_any(): + yield chunk + + return StreamingResponse( + stream_response(), + status_code=resp.status, + media_type=resp.headers.get("content-type", "text/event-stream"), + ) + else: + response_body = await resp.read() + return Response( + content=response_body, + status_code=resp.status, + media_type=resp.headers.get("content-type", "application/json"), + ) + except Exception as e: + traceback.print_exc() + return JSONResponse(status_code=502, content={"error": f"Router failed to reach shard {shard_idx}: {e}"}) + finally: + self._pending_counts[shard_idx] = max(0, self._pending_counts.get(shard_idx, 1) - 1) + + # ===================================================================== + # Server lifecycle + # ===================================================================== + + def start(self) -> None: + """Start the server in a background thread.""" + import uvicorn + + config = uvicorn.Config(self._app, host="0.0.0.0", port=self.port, timeout_keep_alive=120) + server = uvicorn.Server(config) + self._server_thread = threading.Thread(target=server.run, daemon=True) + self._server_thread.start() + print( + f"GenerationControlServer started on port {self.port} " + f"(routing={self.routing_strategy.value}, shards={len(self.shard_urls)})" + ) + + def get_app(self): + """Return the FastAPI app (for testing or custom server setup).""" + return self._app diff --git a/nemo_rl/models/generation/interfaces.py b/nemo_rl/models/generation/interfaces.py index 037b4880f5f..e2a71a390c7 100644 --- a/nemo_rl/models/generation/interfaces.py +++ b/nemo_rl/models/generation/interfaces.py @@ -127,6 +127,11 @@ class GenerationConfig(TypedDict): stop_token_ids: list[int] | None stop_strings: list[str] | None colocated: NotRequired[ColocationConfig] + # Setting `remote_generation_url` with `colocated.enabled=false` enables + # disaggregated HTTP mode: the training cluster sends generation requests + # (via OpenAI /v1/completions) to a separate cluster running the + # GenerationControlServer at this URL. + remote_generation_url: NotRequired[str] # This isn't meant to be passed by the user, but is populated by nemo_rl.models.generation.__init__.configure_generation_config _pad_token_id: NotRequired[int] diff --git a/nemo_rl/models/generation/remote_generation.py b/nemo_rl/models/generation/remote_generation.py new file mode 100644 index 00000000000..a624fc4aecc --- /dev/null +++ b/nemo_rl/models/generation/remote_generation.py @@ -0,0 +1,414 @@ +# 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. +"""RemoteGeneration — GenerationInterface wrapper for disaggregated vLLM. + +Two modes: + + Co-located (``generation`` provided): + Wraps a VllmGeneration instance in the SAME Ray cluster. All calls delegate + to the underlying VllmGeneration; a GenerationControlServer runs alongside + for external HTTP clients (e.g. NemoGym). + + HTTP-only (``generation=None`` + ``server_url``): + VllmGeneration lives in a SEPARATE Ray cluster (or standalone process). + Generation requests are sent to per-shard vLLM workers via the OpenAI + ``/v1/completions`` endpoint (round-robin across shards). Control-plane + calls (weight sync, lifecycle) go to the GenerationControlServer. +""" + +from __future__ import annotations + +import asyncio +import time +from typing import AsyncGenerator, Optional + +import aiohttp +import ray +import requests +import torch + +from nemo_rl.distributed.batched_data_dict import BatchedDataDict +from nemo_rl.models.generation.interfaces import ( + GenerationDatumSpec, + GenerationInterface, + GenerationOutputSpec, +) + +# Timeout for HTTP requests to the control server (seconds). +# Weight sync / NCCL operations can take minutes. +_HTTP_TIMEOUT = 600 + + +@ray.remote(num_cpus=0) +def _http_call_blocking(url: str, json_body: dict | None = None, raw_body: bytes | None = None, timeout: int = _HTTP_TIMEOUT) -> dict: + """Fire-and-forget HTTP POST wrapped in a Ray task. + + Returns the JSON response dict. Using a Ray remote function lets the + caller get back a future immediately, which is critical for NCCL + rendezvous: both training and inference sides must enter simultaneously. + """ + if raw_body is not None: + resp = requests.post(url, data=raw_body, timeout=timeout, headers={"Content-Type": "application/octet-stream"}) + elif json_body is not None: + resp = requests.post(url, json=json_body, timeout=timeout) + else: + resp = requests.post(url, timeout=timeout) + resp.raise_for_status() + return resp.json() + + +class RemoteGeneration(GenerationInterface): + """GenerationInterface wrapper that supports direct delegation or HTTP-only mode.""" + + def __init__( + self, + generation: Optional[GenerationInterface], + server_url: str, + config: dict, + ): + self._generation = generation + self.server_url = server_url.rstrip("/") + self._http_mode = generation is None + + if self._http_mode: + self.cfg = self._fetch_remote_config(config) + else: + self.cfg = dict(generation.cfg) + + # Merge caller-provided overrides + for key in ( + "remote_generation_url", + "max_new_tokens", + "temperature", + "top_p", + "top_k", + "stop_token_ids", + "stop_strings", + ): + if key in config: + self.cfg[key] = config[key] + + # Fetch per-shard vLLM URLs once. JSON completions are sent directly to + # these (round-robin); the control server is used only for control-plane + # and the /v1/* reverse proxy for external clients. + self._shard_urls: list[str] = [] + self._shard_rr_idx = 0 + if self._http_mode: + self._shard_urls = self._fetch_shard_urls() + print( + f" ✓ Disagg HTTP routing to {len(self._shard_urls)} DP shard(s)", + flush=True, + ) + + # Expose the router URL so NemoGym / external clients can reach generation + self.dp_openai_server_base_urls = [f"{self.server_url}/v1"] + + def _fetch_shard_urls(self) -> list[str]: + """Fetch per-shard vLLM URLs from the control server.""" + resp = requests.get(f"{self.server_url}/dp_openai_server_base_urls", timeout=30) + resp.raise_for_status() + urls = [u for u in resp.json() if u is not None] + if not urls: + raise RuntimeError("No shard URLs returned from generation server") + return urls + + def _select_shard(self) -> str: + """Round-robin select a shard URL.""" + url = self._shard_urls[self._shard_rr_idx] + self._shard_rr_idx = (self._shard_rr_idx + 1) % len(self._shard_urls) + return url + + def _fetch_remote_config(self, local_config: dict) -> dict: + """Fetch generation config from the remote control server.""" + for attempt in range(30): + try: + resp = requests.get(f"{self.server_url}/config", timeout=10) + resp.raise_for_status() + remote_cfg = resp.json() + print(f" ✓ Fetched remote generation config from {self.server_url}") + return remote_cfg + except Exception as e: + if attempt < 29: + print(f" Waiting for gen server at {self.server_url} (attempt {attempt + 1}/30): {e}") + time.sleep(5) + else: + raise RuntimeError( + f"Failed to reach generation server at {self.server_url}/config after 30 attempts" + ) from e + + # ===================================================================== + # Data plane — generation + # ===================================================================== + + def generate( + self, data: BatchedDataDict[GenerationDatumSpec], greedy: bool = False + ) -> BatchedDataDict[GenerationOutputSpec]: + if not self._http_mode: + return self._generation.generate(data, greedy) + return asyncio.run(self._generate_json_completions(data, greedy)) + + async def _generate_json_completions( + self, data: BatchedDataDict[GenerationDatumSpec], greedy: bool + ) -> BatchedDataDict[GenerationOutputSpec]: + """Send batch to ONE shard via /v1/completions JSON endpoint (round-robin). + + Uses the OpenAI-compatible completions API with prompt_token_ids. + This goes through vLLM's full OpenAI serving layer (tokenization, validation, etc). + """ + gen_timeout = aiohttp.ClientTimeout(total=300) + + shard_url = self._shard_urls[self._shard_rr_idx] + self._shard_rr_idx = (self._shard_rr_idx + 1) % len(self._shard_urls) + completions_url = f"{shard_url}/completions" + + input_ids = data["input_ids"] + input_lengths = data["input_lengths"] + batch_size = input_ids.shape[0] + max_new_tokens = self.cfg.get("max_new_tokens", 2048) + max_model_len = self.cfg.get("vllm_cfg", {}).get("max_model_len", 4096) + + temperature = 0.0 if greedy else self.cfg.get("temperature", 1.0) + top_p = 1.0 if greedy else self.cfg.get("top_p", 1.0) + + # Build per-sample requests + requests_list = [] + for i in range(batch_size): + length = input_lengths[i].item() + prompt_tokens = input_ids[i, :length].tolist() + max_tokens = min(max_new_tokens, max_model_len - length) + req = { + "model": self.cfg.get("model_name", "default"), + "prompt": prompt_tokens, # vLLM accepts list[int] as token IDs + "max_tokens": max(max_tokens, 1), + "temperature": temperature, + "top_p": top_p, + "logprobs": 1, + } + if self.cfg.get("stop_token_ids"): + req["stop_token_ids"] = self.cfg["stop_token_ids"] + requests_list.append(req) + + # Send all requests concurrently to the same shard + async with aiohttp.ClientSession(timeout=gen_timeout) as session: + async def _send_one(req): + async with session.post(completions_url, json=req) as resp: + resp.raise_for_status() + return await resp.json() + responses = await asyncio.gather(*[_send_one(r) for r in requests_list]) + + # Parse responses into GenerationOutputSpec + pad_token_id = self.cfg.get("_pad_token_id", 0) + all_output_ids = [] + all_gen_lengths = [] + all_unpadded_lengths = [] + all_logprobs = [] + all_truncated = [] + + for i, resp_json in enumerate(responses): + choice = resp_json["choices"][0] + finish_reason = choice.get("finish_reason", "stop") + input_length = input_lengths[i].item() + + # Extract generated token IDs from logprobs.tokens ("token_id:NNN" format) + gen_token_ids = [] + lp_list = [] + logprobs_data = choice.get("logprobs") + if logprobs_data and "tokens" in logprobs_data: + for tok_str in logprobs_data["tokens"]: + if tok_str.startswith("token_id:"): + gen_token_ids.append(int(tok_str.split(":")[1])) + else: + gen_token_ids.append(0) # fallback + lp_list = [lp if lp is not None else 0.0 for lp in logprobs_data.get("token_logprobs", [])] + + gen_length = len(gen_token_ids) + unpadded_length = input_length + gen_length + prompt_tokens = input_ids[i, :input_length].tolist() + full_ids = prompt_tokens + gen_token_ids + + # Pad logprobs: zeros for input tokens, then actual logprobs + full_logprobs = [0.0] * input_length + lp_list + # Pad to same length as full_ids + while len(full_logprobs) < len(full_ids): + full_logprobs.append(0.0) + + all_output_ids.append(full_ids) + all_gen_lengths.append(gen_length) + all_unpadded_lengths.append(unpadded_length) + all_logprobs.append(full_logprobs) + all_truncated.append(finish_reason == "length") + + # Pad to uniform sequence length + max_seq_len = max(len(ids) for ids in all_output_ids) + for i in range(batch_size): + pad_len = max_seq_len - len(all_output_ids[i]) + all_output_ids[i].extend([pad_token_id] * pad_len) + all_logprobs[i].extend([0.0] * pad_len) + + return BatchedDataDict[GenerationOutputSpec]({ + "output_ids": torch.tensor(all_output_ids, dtype=torch.long), + "generation_lengths": torch.tensor(all_gen_lengths, dtype=torch.long), + "unpadded_sequence_lengths": torch.tensor(all_unpadded_lengths, dtype=torch.long), + "logprobs": torch.tensor(all_logprobs, dtype=torch.float32), + "truncated": torch.tensor(all_truncated, dtype=torch.bool), + }) + + async def generate_async( + self, data: BatchedDataDict[GenerationDatumSpec], greedy: bool = False + ) -> AsyncGenerator[tuple[int, BatchedDataDict[GenerationOutputSpec]], None]: + if not self._http_mode: + async for result in self._generation.generate_async(data, greedy): + yield result + return + + result = await self._generate_json_completions(data, greedy) + batch_size = result["output_ids"].shape[0] + for i in range(batch_size): + single = BatchedDataDict[GenerationOutputSpec]({ + k: v[i:i+1] if isinstance(v, torch.Tensor) else ([v[i]] if isinstance(v, list) else v) + for k, v in result.items() + }) + yield (i, single) + + # ===================================================================== + # Weight sync and lifecycle + # ===================================================================== + + def init_collective( + self, ip: str, port: int, world_size: int, *, train_world_size: int + ) -> list[ray.ObjectRef]: + if not self._http_mode: + return self._generation.init_collective( + ip, port, world_size, train_world_size=train_world_size + ) + + # HTTP mode: dispatch as a Ray task so it returns a future. + # The training side calls ray.get(futures_train + futures_inference) + # and both sides must enter NCCL rendezvous simultaneously. + return [ + _http_call_blocking.remote( + f"{self.server_url}/init_collective", + json_body={ + "ip": ip, + "port": port, + "world_size": world_size, + "train_world_size": train_world_size, + }, + ) + ] + + def update_weights_from_collective(self) -> list[ray.ObjectRef]: + if not self._http_mode: + return self._generation.update_weights_from_collective() + + return [ + _http_call_blocking.remote( + f"{self.server_url}/update_weights_from_collective", + ) + ] + + def prepare_for_generation(self, *args: Any, **kwargs: Any) -> bool: + if not self._http_mode: + return self._generation.prepare_for_generation(*args, **kwargs) + + resp = requests.post(f"{self.server_url}/prepare_for_generation", timeout=_HTTP_TIMEOUT) + resp.raise_for_status() + return resp.json().get("success", False) + + def finish_generation(self, *args: Any, **kwargs: Any) -> bool: + if not self._http_mode: + return self._generation.finish_generation(*args, **kwargs) + + resp = requests.post(f"{self.server_url}/finish_generation", timeout=_HTTP_TIMEOUT) + resp.raise_for_status() + return resp.json().get("success", False) + + def prepare_refit_info(self, state_dict_info: dict[str, Any]) -> None: + if not self._http_mode: + self._generation.prepare_refit_info(state_dict_info) + return + + buf = io.BytesIO() + torch.save(state_dict_info, buf) + resp = requests.post( + f"{self.server_url}/prepare_refit_info", + data=buf.getvalue(), + headers={"Content-Type": "application/octet-stream"}, + timeout=_HTTP_TIMEOUT, + ) + resp.raise_for_status() + + def update_weights_via_ipc_zmq(self) -> list[ray.ObjectRef]: + if not self._http_mode: + return self._generation.update_weights_via_ipc_zmq() + raise NotImplementedError("update_weights_via_ipc_zmq not supported in HTTP mode") + + def invalidate_kv_cache(self) -> bool: + if not self._http_mode: + return self._generation.invalidate_kv_cache() + + resp = requests.post(f"{self.server_url}/invalidate_kv_cache", timeout=_HTTP_TIMEOUT) + resp.raise_for_status() + return resp.json().get("success", False) + + @property + def requires_kv_scale_sync(self) -> bool: + if not self._http_mode: + return getattr(self._generation, "requires_kv_scale_sync", False) + return False + + def clear_logger_metrics(self) -> None: + if not self._http_mode: + self._generation.clear_logger_metrics() + return + + try: + requests.post(f"{self.server_url}/clear_logger_metrics", timeout=30) + except Exception: + pass + + def get_logger_metrics(self) -> dict[str, Any]: + if not self._http_mode: + return self._generation.get_logger_metrics() + + try: + resp = requests.get(f"{self.server_url}/get_logger_metrics", timeout=30) + resp.raise_for_status() + return resp.json() + except Exception: + return {} + + def snapshot_step_metrics(self) -> None: + if not self._http_mode: + if hasattr(self._generation, "snapshot_step_metrics"): + self._generation.snapshot_step_metrics() + return + + try: + requests.post(f"{self.server_url}/snapshot_step_metrics", timeout=30) + except Exception: + pass + + def get_step_metrics(self) -> dict[str, float]: + if not self._http_mode: + if hasattr(self._generation, "get_step_metrics"): + return self._generation.get_step_metrics() + return {} + + try: + resp = requests.get(f"{self.server_url}/get_step_metrics", timeout=30) + resp.raise_for_status() + return resp.json() + except Exception: + return {} diff --git a/nemo_rl/models/generation/vllm/vllm_backend.py b/nemo_rl/models/generation/vllm/vllm_backend.py index 51925e40d5e..8c2808213c3 100644 --- a/nemo_rl/models/generation/vllm/vllm_backend.py +++ b/nemo_rl/models/generation/vllm/vllm_backend.py @@ -72,6 +72,19 @@ def init_collective( ) self.model_update_group.init_nccl_communicator(device=self.device) + def reset_collective(self) -> None: + """Tear down the cross-cluster weight-sync collective on this worker. + + Idempotent — a no-op if no collective is currently held. + """ + group = getattr(self, "model_update_group", None) + if group is None: + return + try: + group.destroy() + finally: + self.model_update_group = None # type: ignore[assignment] + def report_device_id(self) -> str: """Retrieve the UUID of the current CUDA device.""" from nemo_rl.utils.nvml import get_device_uuid @@ -337,8 +350,11 @@ def _load_model_weights(weights, model_runner): self._maybe_process_fp8_kv_cache() except Exception as e: + import traceback as _tb print( - f"Error in VllmInternalWorkerExtension.update_weights_from_collective: {e}" + f"Error in VllmInternalWorkerExtension.update_weights_from_collective: {e}\n" + f"{_tb.format_exc()}", + flush=True, ) return False diff --git a/nemo_rl/models/generation/vllm/vllm_generation.py b/nemo_rl/models/generation/vllm/vllm_generation.py index 0faaad17a1b..c2b092decf6 100644 --- a/nemo_rl/models/generation/vllm/vllm_generation.py +++ b/nemo_rl/models/generation/vllm/vllm_generation.py @@ -462,6 +462,23 @@ def init_collective( # this function should co-work with lm_policy, so we should wait for all futures to complete outside return futures + def reset_collective(self) -> list[ray.ObjectRef]: + """Tear down the weight-sync NCCL group across all vLLM workers. + + Idempotent — workers that don't currently hold a group are no-ops. + """ + if not self.worker_group or not self.worker_group.workers: + return [] + method_name = ( + "reset_collective_async" + if self.cfg["vllm_cfg"]["async_engine"] + else "reset_collective" + ) + return self.worker_group.run_all_workers_single_data( + method_name, + run_rank_0_only_axes=["tensor_parallel", "pipeline_parallel"], + ) + def generate( self, data: BatchedDataDict[GenerationDatumSpec], greedy: bool = False ) -> BatchedDataDict[GenerationOutputSpec]: diff --git a/nemo_rl/models/generation/vllm/vllm_worker.py b/nemo_rl/models/generation/vllm/vllm_worker.py index 15935b548af..1e3440ee89d 100644 --- a/nemo_rl/models/generation/vllm/vllm_worker.py +++ b/nemo_rl/models/generation/vllm/vllm_worker.py @@ -688,6 +688,9 @@ def init_collective( ), ) + def reset_collective(self) -> None: + self.llm.collective_rpc("reset_collective") + @wrap_with_nvtx_name("vllm_genertion_worker/generate") def generate( self, data: BatchedDataDict[GenerationDatumSpec], greedy: bool = False diff --git a/nemo_rl/models/generation/vllm/vllm_worker_async.py b/nemo_rl/models/generation/vllm/vllm_worker_async.py index 12f73572b4d..f2651c3aae2 100644 --- a/nemo_rl/models/generation/vllm/vllm_worker_async.py +++ b/nemo_rl/models/generation/vllm/vllm_worker_async.py @@ -509,8 +509,17 @@ async def create_chat_completion( # The request sampling params need to exactly match those as are set in NeMo RL. # If they do not match, the inference will be off policy and destroy training stability. - assert request.temperature == generation_config["temperature"] - assert request.top_p == generation_config["top_p"] + # When params are None (not sent by client), use the generation config defaults. + if request.temperature is None: + request.temperature = generation_config["temperature"] + if request.top_p is None: + request.top_p = generation_config["top_p"] + assert request.temperature == generation_config["temperature"], ( + f"temperature mismatch: request={request.temperature}, config={generation_config['temperature']}" + ) + assert request.top_p == generation_config["top_p"], ( + f"top_p mismatch: request={request.top_p}, config={generation_config['top_p']}" + ) generator = await openai_serving_chat.create_chat_completion( request, raw_request @@ -572,6 +581,41 @@ async def tokenize(request: NeMoRLTokenizeRequest, raw_request: Request): elif isinstance(generator, TokenizeResponse): return JSONResponse(content=generator.model_dump()) + ######################################## + # /v1/completions endpoint (for disaggregated direct-to-shard generation) + ######################################## + from vllm.entrypoints.openai.completion.protocol import ( + CompletionRequest, + CompletionResponse, + ) + from vllm.entrypoints.openai.completion.serving import ( + OpenAIServingCompletion, + ) + + openai_serving_completion = OpenAIServingCompletion( + engine_client=engine_client, + models=openai_serving_models, + request_logger=None, + return_tokens_as_token_ids=True, + ) + + @app.post("/v1/completions") + async def create_completion( + request: CompletionRequest, raw_request: Request + ): + generator = await openai_serving_completion.create_completion( + request, raw_request + ) + + if isinstance(generator, ErrorResponse): + return JSONResponse( + content=generator.model_dump(), status_code=generator.error.code + ) + elif isinstance(generator, CompletionResponse): + return JSONResponse(content=generator.model_dump()) + + return StreamingResponse(content=generator, media_type="text/event-stream") + ######################################## # Logging ######################################## @@ -682,6 +726,9 @@ async def init_collective_async( ), ) + async def reset_collective_async(self) -> None: + await self.llm.collective_rpc("reset_collective") + async def generate_async( self, data: BatchedDataDict[GenerationDatumSpec], diff --git a/tests/unit/models/generation/test_remote_generation_http.py b/tests/unit/models/generation/test_remote_generation_http.py new file mode 100644 index 00000000000..7e441ff936f --- /dev/null +++ b/tests/unit/models/generation/test_remote_generation_http.py @@ -0,0 +1,203 @@ +# 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. +"""Unit tests for the surviving (json-only) code path in RemoteGeneration.""" + +import asyncio +from unittest.mock import MagicMock, patch + +import pytest +import torch + +from nemo_rl.distributed.batched_data_dict import BatchedDataDict +from nemo_rl.models.generation.remote_generation import RemoteGeneration + + +def _make_rg( + shards=("http://shard-0:8000/v1", "http://shard-1:8001/v1"), + model_name="test-model", + max_new_tokens=4, + max_model_len=16, +): + """Build a RemoteGeneration with both HTTP round-trips short-circuited. + + We mock `_fetch_remote_config` and `_fetch_shard_urls` so the constructor + doesn't hit a real server. The returned instance has the surviving json + path fully wired (cfg, shard round-robin, dp_openai_server_base_urls). + """ + fake_cfg = { + "vllm_cfg": {"max_model_len": max_model_len}, + "max_new_tokens": max_new_tokens, + "temperature": 1.0, + "top_p": 1.0, + "model_name": model_name, + } + with ( + patch.object(RemoteGeneration, "_fetch_remote_config", return_value=fake_cfg), + patch.object(RemoteGeneration, "_fetch_shard_urls", return_value=list(shards)), + ): + return RemoteGeneration( + generation=None, + server_url="http://control:8089", + config={}, + ) + + +class _MockResp: + """Minimal async context manager mimicking aiohttp.ClientResponse.""" + + def __init__(self, payload): + self._payload = payload + + async def __aenter__(self): + return self + + async def __aexit__(self, *exc): + return False + + def raise_for_status(self): + return None + + async def json(self): + return self._payload + + +class _MockSession: + """Stand-in for aiohttp.ClientSession that records posts and yields canned responses.""" + + def __init__(self, responses): + self._responses = list(responses) + self.posts: list[tuple[str, dict]] = [] + + async def __aenter__(self): + return self + + async def __aexit__(self, *exc): + return False + + def post(self, url, *, json): + self.posts.append((url, json)) + return _MockResp(self._responses.pop(0)) + + +def _completion_response(token_ids, logprobs=None, finish_reason="stop"): + tokens = [f"token_id:{tid}" for tid in token_ids] + if logprobs is None: + logprobs = [-0.1] * len(token_ids) + return { + "choices": [ + { + "finish_reason": finish_reason, + "logprobs": {"tokens": tokens, "token_logprobs": logprobs}, + } + ] + } + + +def test_constructor_exposes_router_and_shards(): + rg = _make_rg() + assert rg._http_mode is True + assert rg._shard_urls == ["http://shard-0:8000/v1", "http://shard-1:8001/v1"] + # The /v1 on the control URL is what NemoGym calls. + assert rg.dp_openai_server_base_urls == ["http://control:8089/v1"] + # Round-robin starts at 0. + assert rg._shard_rr_idx == 0 + + +def test_generate_json_completions_parses_tokens_and_advances_round_robin(): + rg = _make_rg(max_new_tokens=3, max_model_len=16) + # Two-sample batch: input_lengths 2 and 3, gen 3 tokens each. + data = BatchedDataDict( + { + "input_ids": torch.tensor([[10, 11, 0, 0], [20, 21, 22, 0]]), + "input_lengths": torch.tensor([2, 3]), + } + ) + # First sample: normal tokens + logprobs. Second sample: truncated, one None logprob. + responses = [ + _completion_response([100, 101, 102], logprobs=[-0.1, -0.2, -0.3]), + _completion_response([200, 201, 202], logprobs=[-1.0, None, -0.5], finish_reason="length"), + ] + mock_session = _MockSession(responses) + + with patch("aiohttp.ClientSession", return_value=mock_session): + out = asyncio.run(rg._generate_json_completions(data, greedy=False)) + + # Both requests hit the first shard (round-robin advances once per batch call). + urls = [u for u, _ in mock_session.posts] + assert urls == ["http://shard-0:8000/v1/completions"] * 2 + # Shard idx incremented exactly once. + assert rg._shard_rr_idx == 1 + + # Output shape: 2 samples, prompt length 2/3 + gen 3 = 5/6, pad to 6. + assert out["output_ids"].shape == (2, 6) + # Sample 0: prompt [10,11] + gen [100,101,102] + pad 0. + assert out["output_ids"][0].tolist() == [10, 11, 100, 101, 102, 0] + # Sample 1: prompt [20,21,22] + gen [200,201,202]. + assert out["output_ids"][1].tolist() == [20, 21, 22, 200, 201, 202] + # generation_lengths reflect produced tokens only. + assert out["generation_lengths"].tolist() == [3, 3] + # unpadded = input + gen. + assert out["unpadded_sequence_lengths"].tolist() == [5, 6] + # Truncated only on sample 1 (finish_reason="length"). + assert out["truncated"].tolist() == [False, True] + # logprobs: zeros over prompt tokens, then the vLLM values (None→0.0), then pad zero. + sample0_lp = out["logprobs"][0].tolist() + assert sample0_lp[:2] == [0.0, 0.0] + assert sample0_lp[2:5] == pytest.approx([-0.1, -0.2, -0.3]) + assert sample0_lp[5] == 0.0 + sample1_lp = out["logprobs"][1].tolist() + assert sample1_lp[:3] == [0.0, 0.0, 0.0] + assert sample1_lp[3:6] == pytest.approx([-1.0, 0.0, -0.5]) + + +def test_generate_json_completions_rotates_shards_across_calls(): + rg = _make_rg() + data = BatchedDataDict( + { + "input_ids": torch.tensor([[1, 2]]), + "input_lengths": torch.tensor([2]), + } + ) + responses_first = [_completion_response([99])] + responses_second = [_completion_response([98])] + + with patch("aiohttp.ClientSession", return_value=_MockSession(responses_first)) as s1: + asyncio.run(rg._generate_json_completions(data, greedy=False)) + with patch("aiohttp.ClientSession", return_value=_MockSession(responses_second)) as s2: + asyncio.run(rg._generate_json_completions(data, greedy=False)) + + # First call hits shard 0, second call hits shard 1. + first_url = s1.return_value.posts[0][0] + second_url = s2.return_value.posts[0][0] + assert first_url.startswith("http://shard-0:8000") + assert second_url.startswith("http://shard-1:8001") + + +def test_greedy_overrides_sampling_params(): + rg = _make_rg() + data = BatchedDataDict( + { + "input_ids": torch.tensor([[7, 8]]), + "input_lengths": torch.tensor([2]), + } + ) + mock_session = _MockSession([_completion_response([9])]) + with patch("aiohttp.ClientSession", return_value=mock_session): + asyncio.run(rg._generate_json_completions(data, greedy=True)) + + _, body = mock_session.posts[0] + assert body["temperature"] == 0.0 + assert body["top_p"] == 1.0 + assert body["prompt"] == [7, 8] + assert body["model"] == "test-model" From 191582d4279eca2a0d420dc4e43f899e33b14d66 Mon Sep 17 00:00:00 2001 From: Hemil Desai Date: Fri, 17 Apr 2026 11:11:24 -0700 Subject: [PATCH 80/84] refactor: remove GenerationControlServer DP router MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Training clients already round-robin across DP shards themselves in RemoteGeneration._generate_json_completions, fetched once at startup via the control server's /dp_openai_server_base_urls endpoint. This commit publishes the same shard URL list to NemoGym (via dp_openai_server_base_urls), so the gym's vllm_model plugin — which already supports base_url: Union[str, List[str]] and sticky-maps session -> client round-robin — talks directly to DP shards too. Nothing uses the control server's OpenAI reverse proxy anymore, so delete: * /v1/completions, /v1/chat/completions, /tokenize route handlers * _select_shard, _route_request, _get_session, _pending_counts, _rr_index * RoutingStrategy enum (ROUND_ROBIN / LEAST_PENDING / PREFIX_HASH) * --routing-strategy CLI arg on the standalone launcher * aiohttp/json/hashlib/asyncio/StreamingResponse imports that only the router needed The GenerationControlServer is now a pure control-plane surface: weight sync (init/reset/update_weights_from_collective), lifecycle (prepare_for_generation, finish_generation), refit metadata, logger metrics, and /dp_openai_server_base_urls for shard discovery. Server file shrinks from ~346 → 226 LoC; one fewer network hop on every rollout; one fewer component for NCCL/aiohttp failures to land on. Also restores `import io` in remote_generation.py (dropped too eagerly in the previous commit — still used by prepare_refit_info's torch.save). Tested end-to-end on the 3-cluster disagg IF setup: training registered all 8 DP shard URLs in the endpoint-registry ConfigMap, gym picked them up, first two rollout+training steps completed (44.9s / 53.8s — within the previous router-mediated run's 44.6s mean over 200 steps). Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Hemil Desai Signed-off-by: Terry Kong --- examples/run_standalone_generation_server.py | 173 ++++++++++++++++++ .../generation/generation_control_server.py | 150 ++------------- .../models/generation/remote_generation.py | 12 +- 3 files changed, 196 insertions(+), 139 deletions(-) create mode 100644 examples/run_standalone_generation_server.py diff --git a/examples/run_standalone_generation_server.py b/examples/run_standalone_generation_server.py new file mode 100644 index 00000000000..ce1eb7a9242 --- /dev/null +++ b/examples/run_standalone_generation_server.py @@ -0,0 +1,173 @@ +# 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. +"""Standalone vLLM generation server for disaggregated RL training. + +Entry point for the generation K8s RayCluster. Analogous to nemo_gym.standalone_server. + +Creates a VllmGeneration instance, wraps it in a GenerationControlServer +(control plane + DP shard router), and optionally registers in K8sEndpointRegistry. + +Usage: + python examples/run_standalone_generation_server.py --config + +The server blocks forever. The training cluster drives all lifecycle +(init_collective, weight sync, generation requests) via HTTP. +""" + +import argparse +import os +import pprint +import time + +import ray +from omegaconf import OmegaConf + +from nemo_rl.algorithms.utils import get_tokenizer +from nemo_rl.distributed.virtual_cluster import RayVirtualCluster, _get_node_ip_local, init_ray +from nemo_rl.models.generation import configure_generation_config +from nemo_rl.models.generation.generation_control_server import GenerationControlServer +from nemo_rl.models.generation.vllm import VllmGeneration +from nemo_rl.utils.config import load_config, parse_hydra_overrides, register_omegaconf_resolvers + + +def parse_args(): + parser = argparse.ArgumentParser(description="Standalone vLLM generation server") + parser.add_argument("--config", type=str, required=True, help="Path to config YAML") + parser.add_argument("--port", type=int, default=8089, help="Control server port") + parser.add_argument("--num-gpus", type=int, default=None, help="Number of GPUs to use (default: all available)") + args, overrides = parser.parse_known_args() + return args, overrides + + +def main(): + register_omegaconf_resolvers() + args, overrides = parse_args() + + # Load config + config = load_config(args.config) + if overrides: + config = parse_hydra_overrides(config, overrides) + config = OmegaConf.to_container(config, resolve=True) + + print("Standalone Generation Server — config:") + pprint.pprint(config) + + # Extract generation config + policy_config = config["policy"] + generation_config = policy_config["generation"] + + # Force async engine and HTTP server exposure + generation_config["vllm_cfg"]["async_engine"] = True + generation_config["vllm_cfg"]["expose_http_server"] = True + generation_config["model_name"] = policy_config["model_name"] + + # Non-colocated since this IS the standalone inference server + generation_config.setdefault("colocated", {}) + generation_config["colocated"]["enabled"] = False + + # Initialize Ray + init_ray() + + # Setup tokenizer and configure generation + tokenizer = get_tokenizer(policy_config["tokenizer"]) + generation_config = configure_generation_config(generation_config, tokenizer) + + # Detect available GPUs from the Ray cluster. Count only GPU-capable Ray nodes, + # since KubeRay head pods are often CPU-only and would otherwise skew the division. + cluster_resources = ray.cluster_resources() + num_gpus = int(cluster_resources.get("GPU", 0)) + alive_nodes = [node for node in ray.nodes() if node.get("Alive", False)] + gpu_node_gpu_counts = [ + int(node.get("Resources", {}).get("GPU", 0)) + for node in alive_nodes + if int(node.get("Resources", {}).get("GPU", 0)) > 0 + ] + + colocated_resources = generation_config.get("colocated", {}).get("resources", {}) + configured_num_nodes = colocated_resources.get("num_nodes") + configured_gpus_per_node = colocated_resources.get("gpus_per_node") + + num_nodes = configured_num_nodes or len(gpu_node_gpu_counts) or len(alive_nodes) + gpus_per_node = configured_gpus_per_node or ( + num_gpus // max(len(gpu_node_gpu_counts) or num_nodes, 1) + ) + + assert num_gpus > 0, ( + f"No GPUs available in Ray cluster. Resources: {cluster_resources}" + ) + if args.num_gpus is not None: + num_gpus = min(args.num_gpus, num_gpus) + gpus_per_node = min(gpus_per_node, num_gpus) + print(f"Using {num_gpus} GPUs across {num_nodes} nodes ({gpus_per_node} per node)") + + # Create virtual cluster for inference + cluster = RayVirtualCluster( + name="generation_server_cluster", + bundle_ct_per_node_list=[gpus_per_node] * num_nodes, + use_gpus=True, + num_gpus_per_node=gpus_per_node, + max_colocated_worker_groups=1, + ) + + # Create VllmGeneration (spawns Ray actor workers on GPU nodes) + print("Initializing VllmGeneration...") + t0 = time.perf_counter() + generation = VllmGeneration(cluster=cluster, config=generation_config) + generation.finish_generation() # Reset prefix cache, matches grpo.py init_vllm() pattern + print(f"VllmGeneration initialized in {time.perf_counter() - t0:.1f}s") + + # Start control-plane server (no DP router: clients talk to shards directly). + server = GenerationControlServer( + generation=generation, + port=args.port, + ) + server.start() + + # Register in K8s endpoint registry if disagg_job_id is set + disagg_job_id = os.environ.get("DISAGG_JOB_ID") or generation_config.get("disagg_job_id") + if disagg_job_id: + import json + + from nemo_rl.distributed.k8s_endpoint_registry import K8sEndpointRegistry + + node_ip = _get_node_ip_local() + registry = K8sEndpointRegistry(job_id=disagg_job_id) + registry.create(owner_raycluster_name=os.environ.get("RAY_CLUSTER_NAME")) + registry.set("generation_server_url", f"http://{node_ip}:{args.port}") + registry.set("generation_world_size", str(cluster.world_size())) + registry.set( + "dp_openai_server_base_urls", + json.dumps(generation.dp_openai_server_base_urls), + ) + print(f"Registered in K8sEndpointRegistry (job_id={disagg_job_id})") + + print( + f"\nGeneration server ready on port {args.port}.\n" + f" Router: http://0.0.0.0:{args.port}/v1/completions\n" + f" Control: http://0.0.0.0:{args.port}/health\n" + f" DP shards: {generation.dp_openai_server_base_urls}\n" + f"\nWaiting for training cluster..." + ) + + # Block forever — training cluster drives lifecycle via HTTP + try: + while True: + time.sleep(60) + except KeyboardInterrupt: + print("Shutting down generation server...") + generation.shutdown() + + +if __name__ == "__main__": + main() diff --git a/nemo_rl/models/generation/generation_control_server.py b/nemo_rl/models/generation/generation_control_server.py index 3816ee6b9f5..aee3a8d8d82 100644 --- a/nemo_rl/models/generation/generation_control_server.py +++ b/nemo_rl/models/generation/generation_control_server.py @@ -11,45 +11,37 @@ # 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. -"""Control plane server + DP shard router for disaggregated vLLM generation. +"""Control-plane server for disaggregated vLLM generation. Wraps a local VllmGeneration instance and exposes: - A) Control plane — weight sync triggers, lifecycle management, collective init. - B) DP Shard Router — reverse proxy for vLLM's OpenAI-compatible HTTP API with - intelligent routing across data-parallel shards. - -Runs on a single port (default 8089). Training cluster discovers this one URL. -The training client sends generation requests directly to per-shard -``/v1/completions`` endpoints (round-robin); the reverse proxy here is for -external OpenAI-compatible clients (e.g. NemoGym). + * Weight sync & lifecycle — init/reset/update_weights_from_collective, + prepare_for_generation, finish_generation, prepare_refit_info, + invalidate_kv_cache. + * Shard discovery — ``GET /dp_openai_server_base_urls`` returns the list of + per-shard OpenAI-compatible URLs that clients (training, NemoGym) + round-robin across directly. + +Runs on a single port (default 8089). Training discovers this one URL, pulls +the DP shard list once, and sends generation requests straight to the shards +via OpenAI ``/v1/completions``. The control server never sits on the +generation hot path. """ from __future__ import annotations -import asyncio -import hashlib import io -import json import threading import traceback -from enum import Enum from typing import Any, Optional -import aiohttp import ray import torch -from fastapi import FastAPI, Request, Response -from fastapi.responses import JSONResponse, StreamingResponse +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse from nemo_rl.models.generation.interfaces import GenerationInterface -class RoutingStrategy(str, Enum): - ROUND_ROBIN = "round_robin" - LEAST_PENDING = "least_pending" - PREFIX_HASH = "prefix_hash" - - class GenerationControlServer: """FastAPI server wrapping a VllmGeneration for disaggregated mode.""" @@ -57,11 +49,9 @@ def __init__( self, generation: GenerationInterface, port: int = 8089, - routing_strategy: str = "round_robin", ): self.generation = generation self.port = port - self.routing_strategy = RoutingStrategy(routing_strategy) self.shard_urls: list[str] = [ url @@ -69,22 +59,9 @@ def __init__( if url is not None ] - self._rr_index = 0 - self._pending_counts: dict[int, int] = {i: 0 for i in range(len(self.shard_urls))} - self._lock = asyncio.Lock() - self._session: Optional[aiohttp.ClientSession] = None - self._app = self._build_app() self._server_thread: Optional[threading.Thread] = None - async def _get_session(self) -> aiohttp.ClientSession: - """Lazily create a persistent aiohttp session for the router.""" - if self._session is None or self._session.closed: - self._session = aiohttp.ClientSession( - timeout=aiohttp.ClientTimeout(total=600) - ) - return self._session - def _build_app(self): app = FastAPI(title="Generation Control Server") @@ -225,105 +202,8 @@ async def get_step_metrics(): return self.generation.get_step_metrics() return {} - # ===================================================================== - # DP Shard Router (OpenAI-compatible reverse proxy for external clients) - # ===================================================================== - - @app.post("/v1/completions") - async def route_completions(request: Request): - return await self._route_request(request, "/v1/completions") - - @app.post("/v1/chat/completions") - async def route_chat_completions(request: Request): - return await self._route_request(request, "/v1/chat/completions") - - @app.post("/tokenize") - async def route_tokenize(request: Request): - return await self._route_request(request, "/tokenize") - return app - # ===================================================================== - # Router implementation - # ===================================================================== - - def _select_shard(self, body: dict | None = None) -> int: - """Select a DP shard index based on the routing strategy. - - Safe without a lock: FastAPI runs on a single-threaded asyncio loop, - so _rr_index and _pending_counts mutations are non-concurrent. - """ - if len(self.shard_urls) <= 1: - return 0 - - if self.routing_strategy == RoutingStrategy.ROUND_ROBIN: - idx = self._rr_index - self._rr_index = (self._rr_index + 1) % len(self.shard_urls) - return idx - - if self.routing_strategy == RoutingStrategy.LEAST_PENDING: - return min(self._pending_counts, key=self._pending_counts.get) - - if self.routing_strategy == RoutingStrategy.PREFIX_HASH: - prompt_ids = body.get("prompt_token_ids") if body else None - if prompt_ids: - prefix = tuple(prompt_ids[:128]) - h = int(hashlib.md5(str(prefix).encode()).hexdigest(), 16) - return h % len(self.shard_urls) - return min(self._pending_counts, key=self._pending_counts.get) - - return 0 - - async def _route_request(self, request: Request, path: str): - """Forward an HTTP request to a selected DP shard.""" - body_bytes = await request.body() - - body_dict = None - try: - body_dict = json.loads(body_bytes) - except Exception: - pass - - shard_idx = self._select_shard(body_dict) - shard_base_url = self.shard_urls[shard_idx] - base = shard_base_url.rstrip("/") - if base.endswith("/v1"): - base = base[:-3] - target_url = f"{base}{path}" - - self._pending_counts[shard_idx] = self._pending_counts.get(shard_idx, 0) + 1 - try: - is_streaming = body_dict and body_dict.get("stream", False) - session = await self._get_session() - - async with session.post( - target_url, - data=body_bytes, - headers={"Content-Type": request.headers.get("content-type", "application/json")}, - ) as resp: - if is_streaming: - async def stream_response(): - async for chunk in resp.content.iter_any(): - yield chunk - - return StreamingResponse( - stream_response(), - status_code=resp.status, - media_type=resp.headers.get("content-type", "text/event-stream"), - ) - else: - response_body = await resp.read() - return Response( - content=response_body, - status_code=resp.status, - media_type=resp.headers.get("content-type", "application/json"), - ) - except Exception as e: - traceback.print_exc() - return JSONResponse(status_code=502, content={"error": f"Router failed to reach shard {shard_idx}: {e}"}) - finally: - self._pending_counts[shard_idx] = max(0, self._pending_counts.get(shard_idx, 1) - 1) - # ===================================================================== # Server lifecycle # ===================================================================== @@ -338,7 +218,7 @@ def start(self) -> None: self._server_thread.start() print( f"GenerationControlServer started on port {self.port} " - f"(routing={self.routing_strategy.value}, shards={len(self.shard_urls)})" + f"(shards={len(self.shard_urls)})" ) def get_app(self): diff --git a/nemo_rl/models/generation/remote_generation.py b/nemo_rl/models/generation/remote_generation.py index a624fc4aecc..6d23e817e3f 100644 --- a/nemo_rl/models/generation/remote_generation.py +++ b/nemo_rl/models/generation/remote_generation.py @@ -30,6 +30,7 @@ from __future__ import annotations import asyncio +import io import time from typing import AsyncGenerator, Optional @@ -100,8 +101,8 @@ def __init__( self.cfg[key] = config[key] # Fetch per-shard vLLM URLs once. JSON completions are sent directly to - # these (round-robin); the control server is used only for control-plane - # and the /v1/* reverse proxy for external clients. + # these (round-robin); the control server is only used for control-plane + # calls (weight sync, lifecycle, metrics). self._shard_urls: list[str] = [] self._shard_rr_idx = 0 if self._http_mode: @@ -111,8 +112,11 @@ def __init__( flush=True, ) - # Expose the router URL so NemoGym / external clients can reach generation - self.dp_openai_server_base_urls = [f"{self.server_url}/v1"] + # Expose per-shard URLs so NemoGym / external OpenAI clients can address + # DP shards directly (they round-robin / sticky-map internally). + self.dp_openai_server_base_urls = ( + list(self._shard_urls) if self._http_mode else [f"{self.server_url}/v1"] + ) def _fetch_shard_urls(self) -> list[str]: """Fetch per-shard vLLM URLs from the control server.""" From ced9edb4b9f2aa91f2563c7895275f22b27a09ea Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 19:32:03 -0700 Subject: [PATCH 81/84] feat: nrl-k8s CLI for launching NeMo-RL recipes on Kubernetes (disagg part) Signed-off-by: Terry Kong --- .../grpo_qwen3_4b_instruct_k8s_base.yaml | 94 ++++++++++++++ examples/nemo_gym/run_grpo_nemo_gym.py | 20 ++- examples/run_standalone_generation_server.py | 30 ++++- nemo_rl/algorithms/async_utils.py | 117 +++++++++++------- nemo_rl/algorithms/utils.py | 16 +-- nemo_rl/distributed/k8s_endpoint_registry.py | 51 +++++--- nemo_rl/environments/utils.py | 7 +- .../generation/generation_control_server.py | 47 +++++-- 8 files changed, 296 insertions(+), 86 deletions(-) create mode 100644 examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml diff --git a/examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml b/examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml new file mode 100644 index 00000000000..4051b363791 --- /dev/null +++ b/examples/nemo_gym/grpo_qwen3_4b_instruct_k8s_base.yaml @@ -0,0 +1,94 @@ +defaults: ../configs/grpo_math_1B.yaml + +grpo: + num_prompts_per_step: 4 + num_generations_per_prompt: 2 + max_num_steps: 10 + val_period: 5 + max_val_samples: null + val_batch_size: null + async_grpo: + enabled: true + max_trajectory_age_steps: 1 + in_flight_weight_updates: true + recompute_kv_cache_after_weight_updates: false + +loss_fn: + reference_policy_kl_penalty: 0 + use_importance_sampling_correction: true + +checkpointing: + checkpoint_dir: results/disagg-qwen3-4b-k8s + save_period: 5 + +policy: + model_name: Qwen/Qwen3-4B-Instruct-2507 + tokenizer: + name: ${policy.model_name} + chat_template_kwargs: null + train_global_batch_size: 4 + train_micro_batch_size: 1 + logprob_batch_size: 1 + max_total_sequence_length: 512 + logprob_chunk_size: null + dtensor_cfg: + enabled: true + tensor_parallel_size: 1 + context_parallel_size: 1 + sequence_parallel: false + activation_checkpointing: false + generation: + max_new_tokens: ${policy.max_total_sequence_length} + vllm_cfg: + async_engine: true + tensor_parallel_size: 1 + gpu_memory_utilization: 0.6 + max_model_len: ${policy.max_total_sequence_length} + enforce_eager: false + expose_http_server: true + http_server_serving_chat_kwargs: + enable_auto_tools: true + tool_parser: hermes + colocated: + enabled: false + resources: + gpus_per_node: 8 + num_nodes: 1 + +data: + max_input_seq_length: null + shuffle: false + num_workers: 0 + train: + data_path: ${oc.env:DISAGG_TRAIN_PATH} + dataset_name: NemoGymDataset + split_validation_size: 0 + validation: + data_path: ${oc.env:DISAGG_VALID_PATH} + dataset_name: NemoGymDataset + default: + dataset_name: NemoGymDataset + env_name: nemo_gym + prompt_file: null + system_prompt_file: null + processor: nemo_gym_data_processor + +env: + should_use_nemo_gym: true + nemo_gym: + config_paths: + - responses_api_models/vllm_model/configs/vllm_model_for_training.yaml + - resources_servers/workplace_assistant/configs/workplace_assistant.yaml + +logger: + log_dir: logs/disagg-qwen3-4b-k8s + wandb_enabled: false + tensorboard_enabled: true + monitor_gpus: true + wandb: + project: nemo-rl + name: disagg-gym-qwen3-4b-k8s + +cluster: + gpus_per_node: 1 + num_nodes: 1 diff --git a/examples/nemo_gym/run_grpo_nemo_gym.py b/examples/nemo_gym/run_grpo_nemo_gym.py index 186b4cbf3b0..93efa839854 100644 --- a/examples/nemo_gym/run_grpo_nemo_gym.py +++ b/examples/nemo_gym/run_grpo_nemo_gym.py @@ -242,7 +242,25 @@ def main() -> None: if remote_gym_url: nemo_gym_config["remote_gym_url"] = remote_gym_url print(f"Using remote Gym service at: {remote_gym_url}") - nemo_gym = create_env(env_name="nemo_gym", env_config=nemo_gym_config) + # Schedule NemoGym on a worker node (not the head) to avoid head OOM. + # Find a non-head Ray node and use NodeAffinitySchedulingStrategy. + from ray.util.scheduling_strategies import NodeAffinitySchedulingStrategy + + head_node_id = ray.get_runtime_context().get_node_id() + worker_nodes = [ + n for n in ray.nodes() if n["Alive"] and n["NodeID"] != head_node_id + ] + gym_scheduling = {} + if worker_nodes: + gym_scheduling["scheduling_strategy"] = NodeAffinitySchedulingStrategy( + node_id=worker_nodes[0]["NodeID"], soft=False + ) + print( + f"Scheduling NemoGym on worker node {worker_nodes[0]['NodeManagerAddress']}" + ) + nemo_gym = create_env( + env_name="nemo_gym", env_config=nemo_gym_config, num_cpus=4, **gym_scheduling + ) # Blocking wait for NeMo-Gym to spin up ray.get(nemo_gym.health_check.remote()) diff --git a/examples/run_standalone_generation_server.py b/examples/run_standalone_generation_server.py index ce1eb7a9242..29ebd88021a 100644 --- a/examples/run_standalone_generation_server.py +++ b/examples/run_standalone_generation_server.py @@ -34,18 +34,31 @@ from omegaconf import OmegaConf from nemo_rl.algorithms.utils import get_tokenizer -from nemo_rl.distributed.virtual_cluster import RayVirtualCluster, _get_node_ip_local, init_ray +from nemo_rl.distributed.virtual_cluster import ( + RayVirtualCluster, + _get_node_ip_local, + init_ray, +) from nemo_rl.models.generation import configure_generation_config from nemo_rl.models.generation.generation_control_server import GenerationControlServer from nemo_rl.models.generation.vllm import VllmGeneration -from nemo_rl.utils.config import load_config, parse_hydra_overrides, register_omegaconf_resolvers +from nemo_rl.utils.config import ( + load_config, + parse_hydra_overrides, + register_omegaconf_resolvers, +) def parse_args(): parser = argparse.ArgumentParser(description="Standalone vLLM generation server") parser.add_argument("--config", type=str, required=True, help="Path to config YAML") parser.add_argument("--port", type=int, default=8089, help="Control server port") - parser.add_argument("--num-gpus", type=int, default=None, help="Number of GPUs to use (default: all available)") + parser.add_argument( + "--num-gpus", + type=int, + default=None, + help="Number of GPUs to use (default: all available)", + ) args, overrides = parser.parse_known_args() return args, overrides @@ -135,7 +148,9 @@ def main(): server.start() # Register in K8s endpoint registry if disagg_job_id is set - disagg_job_id = os.environ.get("DISAGG_JOB_ID") or generation_config.get("disagg_job_id") + disagg_job_id = os.environ.get("DISAGG_JOB_ID") or generation_config.get( + "disagg_job_id" + ) if disagg_job_id: import json @@ -150,6 +165,13 @@ def main(): "dp_openai_server_base_urls", json.dumps(generation.dp_openai_server_base_urls), ) + # Backward-compat alias: NemoGym's standalone_server reads + # `vllm_base_urls`. Keep publishing both keys until gym consumers + # migrate to the new name. + registry.set( + "vllm_base_urls", + json.dumps(generation.dp_openai_server_base_urls), + ) print(f"Registered in K8sEndpointRegistry (job_id={disagg_job_id})") print( diff --git a/nemo_rl/algorithms/async_utils.py b/nemo_rl/algorithms/async_utils.py index bcf8a1a188b..5dcd7fc532b 100644 --- a/nemo_rl/algorithms/async_utils.py +++ b/nemo_rl/algorithms/async_utils.py @@ -135,14 +135,31 @@ def sample( min_valid_version = max(0, current_weight_version - max_age_steps) print(f" {min_valid_version=}") - # Check for unexpected old trajectories - old_trajectories = [ - v for v in self.trajectory_versions if v < min_valid_version + # Drop stale trajectories (generated under a policy older than the + # age window). In-flight weight updates can produce these during + # weight-sync windows; discarding is safer than stalling or raising. + stale_idxs = [ + i + for i, v in enumerate(self.trajectory_versions) + if v < min_valid_version ] - if old_trajectories: - raise ValueError( - f"Found {len(old_trajectories)} trajectories older than min_valid_version {min_valid_version}" + if stale_idxs: + print( + f" ⚠️ Dropping {len(stale_idxs)} stale trajectories older than " + f"min_valid_version={min_valid_version} (current_weight_version={current_weight_version}, " + f"max_age_steps={max_age_steps})" ) + keep = [ + i + for i in range(len(self.trajectory_versions)) + if i not in set(stale_idxs) + ] + self.trajectories = [self.trajectories[i] for i in keep] + self.trajectory_versions = [self.trajectory_versions[i] for i in keep] + self.target_weight_versions = [ + self.target_weight_versions[i] for i in keep + ] + total_trajectories = len(self.trajectories) # Filter for valid trajectories without modifying the buffer valid_indices = [ @@ -392,52 +409,58 @@ def start_collection(self, dataloader: StatefulDataLoader) -> None: def _collection_loop(self): """Run the collection loop in background thread.""" try: - for batch in self.dataloader: - if not self.running: - break - - # Check if manually paused and wait - if not self._manual_pause_cleared.is_set() and self.running: - self._manual_pause_cleared.wait() - - # Check if refit is in progress and wait - if not self._refit_pause_cleared.is_set() and self.running: - print("⏸️ Pausing collection for refit...") - self._refit_pause_cleared.wait() - print("▶️ Refit completed, resuming collection") - - # Check if generation limits require pausing collection - if self._should_pause_for_generation_limits() and self.running: - # Only log warning once per weight version - if self._last_limit_warning_version != self.current_weight_version: - async_cfg = self.master_config.get("grpo", {}).get( - "async_grpo", {} - ) - max_trajectory_age = async_cfg["max_trajectory_age_steps"] - target_weights = [ - self.current_weight_version + i - for i in range(max_trajectory_age) - ] - - print( - f"⏸️ Pausing collection: all target weights {target_weights} for weight version {self.current_weight_version} " - f"already exist in buffer. Waiting for weight update..." - ) - self._last_limit_warning_version = self.current_weight_version - - self._generation_limit_cleared.clear() # Clear the event to pause + while self.running: + for batch in self.dataloader: + if not self.running: + break - # Efficiently wait for generation limits to be cleared (no polling!) - self._generation_limit_cleared.wait() + # Check if manually paused and wait + if not self._manual_pause_cleared.is_set() and self.running: + self._manual_pause_cleared.wait() + + # Check if refit is in progress and wait + if not self._refit_pause_cleared.is_set() and self.running: + print("⏸️ Pausing collection for refit...") + self._refit_pause_cleared.wait() + print("▶️ Refit completed, resuming collection") + + # Check if generation limits require pausing collection + if self._should_pause_for_generation_limits() and self.running: + # Only log warning once per weight version + if ( + self._last_limit_warning_version + != self.current_weight_version + ): + async_cfg = self.master_config.get("grpo", {}).get( + "async_grpo", {} + ) + max_trajectory_age = async_cfg["max_trajectory_age_steps"] + target_weights = [ + self.current_weight_version + i + for i in range(max_trajectory_age) + ] + + print( + f"⏸️ Pausing collection: all target weights {target_weights} for weight version {self.current_weight_version} " + f"already exist in buffer. Waiting for weight update..." + ) + self._last_limit_warning_version = ( + self.current_weight_version + ) + + self._generation_limit_cleared.clear() # Clear the event to pause + + # Efficiently wait for generation limits to be cleared (no polling!) + self._generation_limit_cleared.wait() + + # Double-check we're still running after being woken up + if not self.running: + break - # Double-check we're still running after being woken up if not self.running: break - if not self.running: - break - - self._process_batch(batch) + self._process_batch(batch) except Exception as e: print(f"❌ Error in trajectory collection: {e}") diff --git a/nemo_rl/algorithms/utils.py b/nemo_rl/algorithms/utils.py index f46b49ace7d..28700d4738b 100644 --- a/nemo_rl/algorithms/utils.py +++ b/nemo_rl/algorithms/utils.py @@ -513,7 +513,7 @@ def visualize_per_worker_load(per_worker_token_counts: dict[int, int]) -> float: else: per_worker_token_counts = None - if per_worker_token_counts is not None: + if per_worker_token_counts is not None and len(per_worker_token_counts) > 0: average_token_imbalance = visualize_per_worker_load(per_worker_token_counts) performance_metrics["average_token_imbalance"] = average_token_imbalance @@ -565,7 +565,7 @@ def visualize_per_worker_timeline( f" - Timeline (0: {zero_marker}, {', '.join(f'{1.0 if k == 0 else k * (max_value / len(marker))}-{(k + 1) * (max_value / len(marker))}: {marker[k]}' for k in marker.keys())}):" ) for dp_idx, metric_values in metric_dict.items(): - if dp_idx > max_rows_to_print: + if int(dp_idx) > max_rows_to_print: break timeline = [] length = len(metric_values) @@ -589,10 +589,10 @@ def visualize_per_worker_timeline( timeline.append(m) if timeline_interval is not None: print( - f" - Generation Worker {dp_idx:3.0f}: {''.join(timeline)} (Active: {active:.2f} s, Idle: {idle:.2f} s)" + f" - Generation Worker {int(dp_idx):3d}: {''.join(timeline)} (Active: {active:.2f} s, Idle: {idle:.2f} s)" ) else: - print(f" - Generation Worker {dp_idx:3.0f}: {''.join(timeline)}") + print(f" - Generation Worker {int(dp_idx):3d}: {''.join(timeline)}") is_vllm_metrics_logger_enabled = master_config["policy"]["generation"].get( "vllm_cfg", {} @@ -724,16 +724,18 @@ def visualize_per_worker_timeline( metrics["total_num_tokens"] / total_time / total_num_gpus ) policy_training_tokens_per_sec_per_gpu = ( - metrics["total_num_tokens"] / policy_training_time / training_num_gpus + metrics["total_num_tokens"] + / max(policy_training_time, 1e-6) + / training_num_gpus ) policy_and_reference_logprobs_tokens_per_sec_per_gpu = ( metrics["total_num_tokens"] - / policy_and_reference_logprobs_time + / max(policy_and_reference_logprobs_time, 1e-6) / training_num_gpus ) training_worker_group_tokens_per_sec_per_gpu = ( metrics["total_num_tokens"] - / (policy_training_time + policy_and_reference_logprobs_time) + / max(policy_training_time + policy_and_reference_logprobs_time, 1e-6) / training_num_gpus ) generation_tokens_per_sec_per_gpu = ( diff --git a/nemo_rl/distributed/k8s_endpoint_registry.py b/nemo_rl/distributed/k8s_endpoint_registry.py index fd15803712e..47f0ce14c6a 100644 --- a/nemo_rl/distributed/k8s_endpoint_registry.py +++ b/nemo_rl/distributed/k8s_endpoint_registry.py @@ -90,24 +90,45 @@ def create(self, owner_raycluster_name: str | None = None) -> None: print(f"Created endpoint registry ConfigMap: {self.configmap_name}") except ApiException as e: if e.status == 409: - # Already exists — patch ownerReferences if we have them - # (handles race where Gym's set() created it before RL's create()). + # Already exists. Only one RayCluster may be ``controller: true`` + # per object in Kubernetes — if a controller already owns this + # ConfigMap (gen usually registers first), skip the patch instead + # of appending a second controller (which yields 422). if owner_references: - self._v1.patch_namespaced_config_map( - name=self.configmap_name, - namespace=self.namespace, - body=client.V1ConfigMap( - metadata=client.V1ObjectMeta( - owner_references=owner_references, - ) - ), - ) - print( - f"Patched ownerReference on existing ConfigMap: {self.configmap_name}" - ) + try: + existing = self._v1.read_namespaced_config_map( + name=self.configmap_name, + namespace=self.namespace, + ) + has_ctrl = any( + getattr(r, "controller", False) + for r in (existing.metadata.owner_references or []) + ) + except ApiException: + has_ctrl = False + if has_ctrl: + print( + f"Endpoint registry ConfigMap already has a controller; " + f"skipping ownerReference patch for {self.configmap_name}" + ) + else: + self._v1.patch_namespaced_config_map( + name=self.configmap_name, + namespace=self.namespace, + body=client.V1ConfigMap( + metadata=client.V1ObjectMeta( + owner_references=owner_references, + ) + ), + ) + print( + f"Patched ownerReference on existing ConfigMap: " + f"{self.configmap_name}" + ) else: print( - f"Endpoint registry ConfigMap already exists: {self.configmap_name}" + f"Endpoint registry ConfigMap already exists: " + f"{self.configmap_name}" ) else: raise diff --git a/nemo_rl/environments/utils.py b/nemo_rl/environments/utils.py index 1dbd691a583..1a0dab2721d 100644 --- a/nemo_rl/environments/utils.py +++ b/nemo_rl/environments/utils.py @@ -103,7 +103,9 @@ def chunk_list_to_workers(to_chunk: list[Any], num_workers: int) -> list[list[An return chunks -def create_env(env_name: str, env_config: dict) -> EnvironmentInterface: +def create_env( + env_name: str, env_config: dict, **actor_options +) -> EnvironmentInterface: assert env_name in ENV_REGISTRY, ( f"Env name {env_name} is not registered in ENV_REGISTRY. Please call register_env() to register the environment." ) @@ -127,7 +129,8 @@ def create_env(env_name: str, env_config: dict) -> EnvironmentInterface: runtime_env={ "py_executable": actor_py_exec, "env_vars": {**dict(os.environ), **extra_env_vars}, - } + }, + **actor_options, ).remote(env_config) return env diff --git a/nemo_rl/models/generation/generation_control_server.py b/nemo_rl/models/generation/generation_control_server.py index aee3a8d8d82..f81e77d3273 100644 --- a/nemo_rl/models/generation/generation_control_server.py +++ b/nemo_rl/models/generation/generation_control_server.py @@ -29,10 +29,11 @@ from __future__ import annotations +import asyncio import io import threading import traceback -from typing import Any, Optional +from typing import Optional import ray import torch @@ -92,6 +93,7 @@ async def _run_blocking(fn, *args): async def init_collective(request: Request): body = await request.json() try: + def _do(): futures = self.generation.init_collective( ip=body["ip"], @@ -100,11 +102,14 @@ def _do(): train_world_size=body["train_world_size"], ) ray.get(futures) + await _run_blocking(_do) return {"success": True} except Exception as e: traceback.print_exc() - return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + return JSONResponse( + status_code=500, content={"success": False, "error": str(e)} + ) @app.post("/reset_collective") async def reset_collective(): @@ -113,19 +118,24 @@ async def reset_collective(): Idempotent — safe to call when no collective is currently held. """ try: + def _do(): futures = self.generation.reset_collective() if futures: ray.get(futures) + await _run_blocking(_do) return {"success": True} except Exception as e: traceback.print_exc() - return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + return JSONResponse( + status_code=500, content={"success": False, "error": str(e)} + ) @app.post("/update_weights_from_collective") async def update_weights_from_collective(): try: + def _do(): futures = self.generation.update_weights_from_collective() results = ray.get(futures) @@ -135,11 +145,14 @@ def _do(): f"One or more workers failed to update weights. Results: {results}" ) return success + success = await _run_blocking(_do) return {"success": success} except Exception as e: traceback.print_exc() - return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + return JSONResponse( + status_code=500, content={"success": False, "error": str(e)} + ) @app.post("/prepare_for_generation") async def prepare_for_generation(): @@ -148,7 +161,9 @@ async def prepare_for_generation(): return {"success": bool(result)} except Exception as e: traceback.print_exc() - return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + return JSONResponse( + status_code=500, content={"success": False, "error": str(e)} + ) @app.post("/finish_generation") async def finish_generation(): @@ -157,20 +172,28 @@ async def finish_generation(): return {"success": bool(result)} except Exception as e: traceback.print_exc() - return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + return JSONResponse( + status_code=500, content={"success": False, "error": str(e)} + ) @app.post("/prepare_refit_info") async def prepare_refit_info(request: Request): try: body_bytes = await request.body() + def _do(): - state_dict_info = torch.load(io.BytesIO(body_bytes), weights_only=False) + state_dict_info = torch.load( + io.BytesIO(body_bytes), weights_only=False + ) self.generation.prepare_refit_info(state_dict_info) + await _run_blocking(_do) return {"success": True} except Exception as e: traceback.print_exc() - return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + return JSONResponse( + status_code=500, content={"success": False, "error": str(e)} + ) @app.post("/invalidate_kv_cache") async def invalidate_kv_cache(): @@ -179,7 +202,9 @@ async def invalidate_kv_cache(): return {"success": bool(result)} except Exception as e: traceback.print_exc() - return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + return JSONResponse( + status_code=500, content={"success": False, "error": str(e)} + ) @app.post("/clear_logger_metrics") async def clear_logger_metrics(): @@ -212,7 +237,9 @@ def start(self) -> None: """Start the server in a background thread.""" import uvicorn - config = uvicorn.Config(self._app, host="0.0.0.0", port=self.port, timeout_keep_alive=120) + config = uvicorn.Config( + self._app, host="0.0.0.0", port=self.port, timeout_keep_alive=120 + ) server = uvicorn.Server(config) self._server_thread = threading.Thread(target=server.run, daemon=True) self._server_thread.start() From 3d502b4209369c06d25ca9b22b059b209203ad29 Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 16:26:37 -0700 Subject: [PATCH 82/84] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94?= =?UTF-8?q?=20revert=20stray=20algorithm=20changes,=20fix=20types=20and=20?= =?UTF-8?q?exceptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Revert async_utils.py and utils.py (algorithm changes unrelated to infra) - torch.load weights_only=True in generation_control_server.py - Narrow except Exception to requests.RequestException in remote_generation.py - Add pragma: no cover on ray.remote function in remote_generation.py - Fix return type annotation on k8s_endpoint_registry._build_owner_reference Signed-off-by: Terry Kong --- nemo_rl/algorithms/async_utils.py | 117 +++++++----------- nemo_rl/algorithms/utils.py | 16 ++- nemo_rl/distributed/k8s_endpoint_registry.py | 2 +- .../generation/generation_control_server.py | 2 +- .../models/generation/remote_generation.py | 10 +- 5 files changed, 61 insertions(+), 86 deletions(-) diff --git a/nemo_rl/algorithms/async_utils.py b/nemo_rl/algorithms/async_utils.py index 5dcd7fc532b..bcf8a1a188b 100644 --- a/nemo_rl/algorithms/async_utils.py +++ b/nemo_rl/algorithms/async_utils.py @@ -135,31 +135,14 @@ def sample( min_valid_version = max(0, current_weight_version - max_age_steps) print(f" {min_valid_version=}") - # Drop stale trajectories (generated under a policy older than the - # age window). In-flight weight updates can produce these during - # weight-sync windows; discarding is safer than stalling or raising. - stale_idxs = [ - i - for i, v in enumerate(self.trajectory_versions) - if v < min_valid_version + # Check for unexpected old trajectories + old_trajectories = [ + v for v in self.trajectory_versions if v < min_valid_version ] - if stale_idxs: - print( - f" ⚠️ Dropping {len(stale_idxs)} stale trajectories older than " - f"min_valid_version={min_valid_version} (current_weight_version={current_weight_version}, " - f"max_age_steps={max_age_steps})" + if old_trajectories: + raise ValueError( + f"Found {len(old_trajectories)} trajectories older than min_valid_version {min_valid_version}" ) - keep = [ - i - for i in range(len(self.trajectory_versions)) - if i not in set(stale_idxs) - ] - self.trajectories = [self.trajectories[i] for i in keep] - self.trajectory_versions = [self.trajectory_versions[i] for i in keep] - self.target_weight_versions = [ - self.target_weight_versions[i] for i in keep - ] - total_trajectories = len(self.trajectories) # Filter for valid trajectories without modifying the buffer valid_indices = [ @@ -409,58 +392,52 @@ def start_collection(self, dataloader: StatefulDataLoader) -> None: def _collection_loop(self): """Run the collection loop in background thread.""" try: - while self.running: - for batch in self.dataloader: - if not self.running: - break + for batch in self.dataloader: + if not self.running: + break - # Check if manually paused and wait - if not self._manual_pause_cleared.is_set() and self.running: - self._manual_pause_cleared.wait() - - # Check if refit is in progress and wait - if not self._refit_pause_cleared.is_set() and self.running: - print("⏸️ Pausing collection for refit...") - self._refit_pause_cleared.wait() - print("▶️ Refit completed, resuming collection") - - # Check if generation limits require pausing collection - if self._should_pause_for_generation_limits() and self.running: - # Only log warning once per weight version - if ( - self._last_limit_warning_version - != self.current_weight_version - ): - async_cfg = self.master_config.get("grpo", {}).get( - "async_grpo", {} - ) - max_trajectory_age = async_cfg["max_trajectory_age_steps"] - target_weights = [ - self.current_weight_version + i - for i in range(max_trajectory_age) - ] - - print( - f"⏸️ Pausing collection: all target weights {target_weights} for weight version {self.current_weight_version} " - f"already exist in buffer. Waiting for weight update..." - ) - self._last_limit_warning_version = ( - self.current_weight_version - ) - - self._generation_limit_cleared.clear() # Clear the event to pause - - # Efficiently wait for generation limits to be cleared (no polling!) - self._generation_limit_cleared.wait() - - # Double-check we're still running after being woken up - if not self.running: - break + # Check if manually paused and wait + if not self._manual_pause_cleared.is_set() and self.running: + self._manual_pause_cleared.wait() + # Check if refit is in progress and wait + if not self._refit_pause_cleared.is_set() and self.running: + print("⏸️ Pausing collection for refit...") + self._refit_pause_cleared.wait() + print("▶️ Refit completed, resuming collection") + + # Check if generation limits require pausing collection + if self._should_pause_for_generation_limits() and self.running: + # Only log warning once per weight version + if self._last_limit_warning_version != self.current_weight_version: + async_cfg = self.master_config.get("grpo", {}).get( + "async_grpo", {} + ) + max_trajectory_age = async_cfg["max_trajectory_age_steps"] + target_weights = [ + self.current_weight_version + i + for i in range(max_trajectory_age) + ] + + print( + f"⏸️ Pausing collection: all target weights {target_weights} for weight version {self.current_weight_version} " + f"already exist in buffer. Waiting for weight update..." + ) + self._last_limit_warning_version = self.current_weight_version + + self._generation_limit_cleared.clear() # Clear the event to pause + + # Efficiently wait for generation limits to be cleared (no polling!) + self._generation_limit_cleared.wait() + + # Double-check we're still running after being woken up if not self.running: break - self._process_batch(batch) + if not self.running: + break + + self._process_batch(batch) except Exception as e: print(f"❌ Error in trajectory collection: {e}") diff --git a/nemo_rl/algorithms/utils.py b/nemo_rl/algorithms/utils.py index 28700d4738b..f46b49ace7d 100644 --- a/nemo_rl/algorithms/utils.py +++ b/nemo_rl/algorithms/utils.py @@ -513,7 +513,7 @@ def visualize_per_worker_load(per_worker_token_counts: dict[int, int]) -> float: else: per_worker_token_counts = None - if per_worker_token_counts is not None and len(per_worker_token_counts) > 0: + if per_worker_token_counts is not None: average_token_imbalance = visualize_per_worker_load(per_worker_token_counts) performance_metrics["average_token_imbalance"] = average_token_imbalance @@ -565,7 +565,7 @@ def visualize_per_worker_timeline( f" - Timeline (0: {zero_marker}, {', '.join(f'{1.0 if k == 0 else k * (max_value / len(marker))}-{(k + 1) * (max_value / len(marker))}: {marker[k]}' for k in marker.keys())}):" ) for dp_idx, metric_values in metric_dict.items(): - if int(dp_idx) > max_rows_to_print: + if dp_idx > max_rows_to_print: break timeline = [] length = len(metric_values) @@ -589,10 +589,10 @@ def visualize_per_worker_timeline( timeline.append(m) if timeline_interval is not None: print( - f" - Generation Worker {int(dp_idx):3d}: {''.join(timeline)} (Active: {active:.2f} s, Idle: {idle:.2f} s)" + f" - Generation Worker {dp_idx:3.0f}: {''.join(timeline)} (Active: {active:.2f} s, Idle: {idle:.2f} s)" ) else: - print(f" - Generation Worker {int(dp_idx):3d}: {''.join(timeline)}") + print(f" - Generation Worker {dp_idx:3.0f}: {''.join(timeline)}") is_vllm_metrics_logger_enabled = master_config["policy"]["generation"].get( "vllm_cfg", {} @@ -724,18 +724,16 @@ def visualize_per_worker_timeline( metrics["total_num_tokens"] / total_time / total_num_gpus ) policy_training_tokens_per_sec_per_gpu = ( - metrics["total_num_tokens"] - / max(policy_training_time, 1e-6) - / training_num_gpus + metrics["total_num_tokens"] / policy_training_time / training_num_gpus ) policy_and_reference_logprobs_tokens_per_sec_per_gpu = ( metrics["total_num_tokens"] - / max(policy_and_reference_logprobs_time, 1e-6) + / policy_and_reference_logprobs_time / training_num_gpus ) training_worker_group_tokens_per_sec_per_gpu = ( metrics["total_num_tokens"] - / max(policy_training_time + policy_and_reference_logprobs_time, 1e-6) + / (policy_training_time + policy_and_reference_logprobs_time) / training_num_gpus ) generation_tokens_per_sec_per_gpu = ( diff --git a/nemo_rl/distributed/k8s_endpoint_registry.py b/nemo_rl/distributed/k8s_endpoint_registry.py index 47f0ce14c6a..df9dd702c6e 100644 --- a/nemo_rl/distributed/k8s_endpoint_registry.py +++ b/nemo_rl/distributed/k8s_endpoint_registry.py @@ -228,7 +228,7 @@ def signal_error(self, message: str) -> None: def _build_owner_reference( self, raycluster_name: str - ) -> list[client.V1OwnerReference]: + ) -> list[client.V1OwnerReference] | None: """Look up the RayCluster's UID and build an ownerReference.""" try: rc = self._custom.get_namespaced_custom_object( diff --git a/nemo_rl/models/generation/generation_control_server.py b/nemo_rl/models/generation/generation_control_server.py index f81e77d3273..b4aa845c9b7 100644 --- a/nemo_rl/models/generation/generation_control_server.py +++ b/nemo_rl/models/generation/generation_control_server.py @@ -183,7 +183,7 @@ async def prepare_refit_info(request: Request): def _do(): state_dict_info = torch.load( - io.BytesIO(body_bytes), weights_only=False + io.BytesIO(body_bytes), weights_only=True ) self.generation.prepare_refit_info(state_dict_info) diff --git a/nemo_rl/models/generation/remote_generation.py b/nemo_rl/models/generation/remote_generation.py index 6d23e817e3f..4345f9d57a4 100644 --- a/nemo_rl/models/generation/remote_generation.py +++ b/nemo_rl/models/generation/remote_generation.py @@ -52,7 +52,7 @@ @ray.remote(num_cpus=0) -def _http_call_blocking(url: str, json_body: dict | None = None, raw_body: bytes | None = None, timeout: int = _HTTP_TIMEOUT) -> dict: +def _http_call_blocking(url: str, json_body: dict | None = None, raw_body: bytes | None = None, timeout: int = _HTTP_TIMEOUT) -> dict: # pragma: no cover """Fire-and-forget HTTP POST wrapped in a Ray task. Returns the JSON response dict. Using a Ray remote function lets the @@ -379,7 +379,7 @@ def clear_logger_metrics(self) -> None: try: requests.post(f"{self.server_url}/clear_logger_metrics", timeout=30) - except Exception: + except requests.RequestException: pass def get_logger_metrics(self) -> dict[str, Any]: @@ -390,7 +390,7 @@ def get_logger_metrics(self) -> dict[str, Any]: resp = requests.get(f"{self.server_url}/get_logger_metrics", timeout=30) resp.raise_for_status() return resp.json() - except Exception: + except requests.RequestException: return {} def snapshot_step_metrics(self) -> None: @@ -401,7 +401,7 @@ def snapshot_step_metrics(self) -> None: try: requests.post(f"{self.server_url}/snapshot_step_metrics", timeout=30) - except Exception: + except requests.RequestException: pass def get_step_metrics(self) -> dict[str, float]: @@ -414,5 +414,5 @@ def get_step_metrics(self) -> dict[str, float]: resp = requests.get(f"{self.server_url}/get_step_metrics", timeout=30) resp.raise_for_status() return resp.json() - except Exception: + except requests.RequestException: return {} From 51068d1692fecd3fb8d94c904a6213295e8ef93d Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Thu, 23 Apr 2026 22:40:17 -0700 Subject: [PATCH 83/84] =?UTF-8?q?chore:=20sanitize=20qwen3=5F4b=20configs?= =?UTF-8?q?=20=E2=80=94=20generic=20node=20names,=20main=20branch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Terry Kong --- infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml | 4 ++-- .../examples/qwen3_4b_if_single.gb300.prod.infra.yaml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml b/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml index 7f23fabe063..876bdf60ab2 100644 --- a/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml +++ b/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.infra.yaml @@ -1,4 +1,4 @@ -# Infra for qwen3_4b_if_single.yaml on a GB300 (p6e-gb300r.36xlarge) EKS cluster. +# Infra for qwen3_4b_if_single.yaml on a GB300 NVL72 EKS cluster. # # Per-node hardware: 4× NVIDIA-GB300, ~140 CPU, ~925 GiB memory, arm64. GB300 # nodes advertise no taints on this cluster — plain nodeSelector is enough. @@ -26,7 +26,7 @@ _shared: limits: {cpu: "8", memory: "32Gi"} requests: {cpu: "2", memory: "8Gi"} gpuWorkerResources: &gpu_worker_resources - # p6e-gb300r.36xlarge allocatable: ~139.6 CPU, ~924 GiB, 4 GPUs. + # NVL72 GB300 node allocatable: ~140 CPU / ~924 GiB / 4 GPUs. # Claim 128 CPU / 880 GiB to leave headroom for the Ray head pod + # gpu-operator / device-plugin / dgxc daemonsets (~11 CPU, ~40 GiB). limits: diff --git a/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml b/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml index abb983c2b97..7b45bdfbad8 100644 --- a/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml +++ b/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml @@ -1,7 +1,7 @@ # Prod-mode infra for qwen3_4b_if_single.yaml on GB300. # # Same topology as qwen3_4b_if_single.gb300.infra.yaml (1 node × 4 GPUs on -# p6e-gb300r.36xlarge), but submission is prod-shaped: +# NVL72 GB300 node), but submission is prod-shaped: # # submit.submitter portForward — Ray Job SDK via kubectl port-forward. # launch.runMode batch — CLI returns after Ray accepts the job. @@ -18,7 +18,7 @@ # Prereqs (onboarding §3–§5 already applied in `default`): # - rl-workspace PVC bound (shared FSx Lustre, 2.4 TiB, RWX) # - /mnt/rl-workspace/${user:}/nemo-rl contains a git checkout of -# https://github.com/NVIDIA-NeMo/RL on branch hemil/k8s-infra-cp +# https://github.com/NVIDIA-NeMo/RL on branch main # - nvcr-secret, wandb-api-key, nemo-rl-endpoint-registry all present # # Usage: @@ -49,7 +49,7 @@ _shared: limits: {cpu: "8", memory: "32Gi"} requests: {cpu: "2", memory: "8Gi"} gpuWorkerResources: &gpu_worker_resources - # p6e-gb300r.36xlarge allocatable: ~139.6 CPU / ~924 GiB / 4 GPUs. + # NVL72 GB300 node allocatable: ~140 CPU / ~924 GiB / 4 GPUs. limits: cpu: "128" memory: "880Gi" From 6199d67771af4cc022a81678cc8b6e07eec9de7c Mon Sep 17 00:00:00 2001 From: Terry Kong Date: Sun, 26 Apr 2026 11:43:23 -0700 Subject: [PATCH 84/84] chore: fix stale tools/nrl_k8s paths in qwen3_4b examples Signed-off-by: Terry Kong --- .../examples/qwen3_4b_if_full_disagg.infra.yaml | 4 ++-- .../qwen3_4b_if_gym_disagg.prod.infra.yaml | 16 ++++++++-------- .../qwen3_4b_if_single.gb300.prod.infra.yaml | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml b/infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml index c9dd942b71f..673e758cc97 100644 --- a/infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml +++ b/infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml @@ -1,8 +1,8 @@ # Infra for the Qwen3-4B full-disaggregated run — paired with qwen3_4b_if_full_disagg.yaml. # # Usage: -# nrl-k8s run tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml +# nrl-k8s run infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.yaml \ +# --infra infra/nrl_k8s/examples/qwen3_4b_if_full_disagg.infra.yaml # # Everything K8s-specific lives here so the recipe stays cluster-agnostic. # Shared YAML anchors below keep the three cluster specs from repeating. diff --git a/infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml b/infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml index 86adb1b23a9..9b558812ad1 100644 --- a/infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml +++ b/infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml @@ -8,25 +8,25 @@ # # Usage: # # Admins, once: bring up the two RayClusters. -# nrl-k8s cluster up tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ +# nrl-k8s cluster up infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ +# --infra infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ # --role gym -# nrl-k8s cluster up tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ +# nrl-k8s cluster up infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ +# --infra infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ # --role training # # # Researcher, per run — idempotent; reuses live clusters if matching, # # submits training against the training cluster. Returns in seconds, # # laptop disconnectable. Use --skip-daemons after first bring-up if # # gym/generation are already healthy. -# nrl-k8s run tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ +# nrl-k8s run infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ +# --infra infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ # --run-id qwen3-4b-gym-disagg-$(date +%Y%m%d-%H%M%S) # # # Observe from anywhere; handle is cached on disk. # nrl-k8s job logs \ -# tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ +# infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.yaml \ +# --infra infra/nrl_k8s/examples/qwen3_4b_if_gym_disagg.prod.infra.yaml \ # --role training -f _shared: diff --git a/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml b/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml index 7b45bdfbad8..a8f539d23aa 100644 --- a/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml +++ b/infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml @@ -24,13 +24,13 @@ # Usage: # # Long-lived cluster — idempotent; reuses the live cluster if it # # already matches, applies if absent. Submits training per invocation. -# nrl-k8s run tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml \ +# nrl-k8s run infra/nrl_k8s/examples/qwen3_4b_if_single.yaml \ +# --infra infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml \ # --run-id single-if-$(date +%Y%m%d-%H%M%S) # # # One-shot with auto-teardown (ephemeral RayCluster): -# nrl-k8s rayjob tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ -# --infra tools/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml +# nrl-k8s rayjob infra/nrl_k8s/examples/qwen3_4b_if_single.yaml \ +# --infra infra/nrl_k8s/examples/qwen3_4b_if_single.gb300.prod.infra.yaml _shared: nodeSelector: &shared_node_selector @@ -124,7 +124,7 @@ launch: mkdir -p "\${LOG_DIR}" python -u examples/nemo_gym/run_grpo_nemo_gym.py \ - --config tools/nrl_k8s/examples/qwen3_4b_if_single.yaml \ + --config infra/nrl_k8s/examples/qwen3_4b_if_single.yaml \ ++env.nemo_gym.config_paths='[responses_api_models/vllm_model/configs/vllm_model_for_training.yaml,resources_servers/instruction_following/configs/instruction_following.yaml]' \ ~policy.optimizer.kwargs.foreach \ ~policy.optimizer.kwargs.fused \