diff --git a/.gitignore b/.gitignore index 505a3b1..259d473 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,33 @@ dist/ wheels/ *.egg-info +# mac specific crap +.DS_Store + +# checkpoints +checkpoints_*/ + # Virtual environments .venv + +# wandb files +wandb/ + +# Slurm logs +logs/ +*.log + +# Big jsonl files +data/ +*.jsonl + +# Environment files (secrets) +.env +.env.* +.DS_Store + +# HPC specific files +examples/code_exec/hpc/ + +# personal research directory +research/ diff --git a/.python-version b/.python-version deleted file mode 100644 index e4fba21..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.12 diff --git a/AGENTS.md b/AGENTS.md index 48c9080..69fe02f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,9 +20,10 @@ Instead, Ludic is closer to **classical RL** – specifically policy-gradient me - **Separate Agent vs Environment** - **Environment** = state transition function (+ optional scalar reward) with minimal assumptions; can be multi-agent by default. - - **Agent** = LLM *with state* (prompt harness + memory + parsing + optional auxiliary tools). - - auxiliary tools = tools that don't change the state of the environment - - Rationale: reuse environments across different “agent harnesses” (memory schemes, parsers, prompts, tools) and reuse harness pieces across environments. + - **Agent** = LLM *with state* (prompt harness + memory + parsing + optional tools). + - Internal tools = executed by agent (calculator, code interpreter); don't change env state + - External tools = returned to protocol for handling (delegation, sub-agents) + - Rationale: reuse environments across different "agent harnesses" (memory schemes, parsers, prompts, tools) and reuse harness pieces across environments. - **Make the interaction loop explicit** - Neither env nor agent “owns” rollout generation. An **InteractionProtocol** owns the agent<-->env loop and produces rollouts. @@ -50,6 +51,11 @@ Instead, Ludic is closer to **classical RL** – specifically policy-gradient me ## Core Abstractions (Where + What) - **Shared types (rollouts, steps, truncation flags)**: `src/ludic/types.py` +- **Steps (agent vs env)**: See `CONSIDERATIONS.md` for the full rationale. The short version: + - **AgentStep**: Every model call, including internal tool loops. Contains `TokenTrace` for training. + - **EnvironmentStep**: State transitions (`env.step()` outcomes). References the triggering AgentSteps. + - Why separate? Training needs the full reasoning trace, not just final actions. A ReAct agent might call 3 tools before outputting an action—all those calls have token traces we want to train on. + - Rollouts keep a single timeline of both kinds; online batching concatenates all AgentSteps in a turn into one `SAWItem`. - **Environment kernel (multi-agent by default)**: `src/ludic/envs/env.py` - `LudicEnv.reset() -> {agent_id: (obs, info)}` @@ -62,8 +68,10 @@ Instead, Ludic is closer to **classical RL** – specifically policy-gradient me - Wraps a `ChatClient` (inference backend), a `ContextStrategy` (memory/prompt building), and a `Parser` (action decoding + intrinsic format rewards/penalties). - Handles incomplete completions (`finish_reason == "length"`) as parse failures (optional) to avoid training on truncated actions. - Extended agent types: - - `ToolAgent` (`src/ludic/agents/tool_agent.py`): OpenAI/vLLM-compatible tool calling with automatic schema generation from callables. - - `ReActAgent` (`src/ludic/agents/react_agent.py`): Multi-step ReAct pattern with configurable `max_react_steps` for tool loops. + - `ToolAgent` (`src/ludic/agents/tool_agent.py`): Base for tool-calling agents. Supports two tool scopes: + - `tools`: Internal tools executed by agent (calculator, code interpreter). Results go to context, agent continues. + - `external_tools`: Tools returned to protocol for handling (delegation, sub-agents). Protocol feeds results back. + - `ReActAgent` (`src/ludic/agents/react_agent.py`): Multi-step ReAct pattern [Think → Tool]* → Act. Returns `action_target` indicating what happens next: `"internal"` (handled), `"external"` (protocol handles), or `"env"` (final action). - **Context strategy (memory/prompt policy)**: `src/ludic/context/base.py` - Hooks: `on_env_reset`, `on_before_act`, `on_after_act`, `on_after_step`. @@ -77,8 +85,12 @@ Instead, Ludic is closer to **classical RL** – specifically policy-gradient me - **Interaction protocols (own the loop)**: `src/ludic/interaction/base.py` - Single-agent synchronous loop: `src/ludic/interaction/single_agent.py` + - Supports `external_tool_handler` callback for handling external tool calls - Multi-agent loop (per-agent rollouts via `TraceCollector`): `src/ludic/interaction/multi_agent.py`, `src/ludic/interaction/step_collector.py` - - Key behavior: parser failures are handled *inside the protocol* (synthetic step, no `env.step()` call), so env stays parser-agnostic. + - Key behaviors: + - Parser failures are handled *inside the protocol* (synthetic step, no `env.step()` call), so env stays parser-agnostic. + - External tool calls (`action_target="external"`) are routed through `external_tool_handler`; results are fed back to agent context and the agent continues reasoning. + - **Delegation pattern**: External tools enable hierarchical agents where a parent can spawn sub-agents. The protocol handles the sub-agent's rollout and returns results to the parent. Both rollouts are collected for training. See `CONSIDERATIONS.md` for details. - Utility: `src/ludic/interaction/info.py` provides `merge_step_info()` for safely merging step metadata with collision detection on reserved keys. - **Rollout execution + collation**: `src/ludic/training/batching/rollout_engine.py` @@ -86,7 +98,7 @@ Instead, Ludic is closer to **classical RL** – specifically policy-gradient me - Converts rollouts → `SAWItem`s using either: - exact token IDs returned by the inference backend (preferred), or - `retokenize=True` with a caller-provided tokenizer. - - Practical note: if you want drift-free RL on the *actual sampled tokens*, have your inference client return token IDs/logprobs (vLLM: `SamplingArgs["extras"]["extra_body"]["return_token_ids"]=True`). + - Practical note: Token-in mode (see README) ensures drift-free RL by using rollout-time token IDs directly. Use `ReturnSpec.for_rl()` or set `return_token_ids=True` in `InferenceSpec` to get token IDs from the backend. - **Batch sources (trainer talks to these, not the engine)**: `src/ludic/training/types.py` - Sync: `src/ludic/training/batching/synced_batching.py` (`RolloutBatchSource`) @@ -96,9 +108,9 @@ Instead, Ludic is closer to **classical RL** – specifically policy-gradient me - **Algorithm injection (credit + loss)**: `src/ludic/training/algorithm.py` - `RLAlgorithm = (CreditAssigner, Loss)` - - Presets: `make_reinforce()`, `make_reinforce_baseline()`, `make_grpo()`, `make_sft()` + - Presets: `make_reinforce()`, `make_reinforce_baseline()`, `make_grpo()`, `make_dr_grpo()`, `make_gspo()`, `make_cispo()`, `make_gmpo()`, `make_sft()` - Credit assigners: `src/ludic/training/credit_assignment.py` – `MonteCarloReturn`, `GroupNormalizedReturn`, `EpisodicReturn`, `PerStepReward`, `ConstantCredit` - - Losses: `src/ludic/training/loss.py` + - Losses: `src/ludic/training/loss.py` – `ReinforceLoss`, `TokenClippedSurrogateLoss`, `ClippedSurrogateLoss`, `CISPOLoss`, `GMPOLoss`, `MaskedCausalLMCrossEntropyLoss` - **Trainer (optimization loop only)**: `src/ludic/training/trainer.py` - Collates `SAWItem` → tensors and runs `RLAlgorithm.loss`. @@ -135,6 +147,23 @@ GRPO mental model in this codebase: - It avoids a learned **value function** by using a **Monte Carlo / group-relative baseline** (group mean reward for the same prompt) to form advantages. - If you come from PPO-RLHF: think "PPO-shaped dataflow" without a critic/value model, where the "advantage" is estimated by group comparison rather than by GAE/value bootstrapping. +## GMPO (Geometric-Mean Policy Optimization) + +**GMPO** (arXiv:2507.20673) is a variant of GRPO that uses the **geometric mean** of token-level importance ratios instead of the arithmetic mean. + +**Core idea**: +- GRPO optimizes: (1/|o|) Σ_t ρ_t * A (arithmetic mean) +- GMPO optimizes: (∏_t ρ_t)^(1/|o|) * A (geometric mean) + +The geometric mean is less sensitive to outlier importance ratios, which can help prevent extreme policy updates when individual tokens have unusually high or low ratios. + +**Implementation** (`src/ludic/training/loss.py`, `src/ludic/training/algorithm.py`): +- **Loss**: `GMPOLoss` computes the geometric mean in log-space for numerical stability +- **Objective**: J_GMPO = E[ (∏_t min(ρ_t * A, clip(ρ_t, e^-ε_low, e^ε_high) * A))^(1/|o|) * sgn(A) ] +- **Clipping**: Token-level clipping in log-space, wider default range (e^-0.4, e^0.4) vs GRPO's (0.8, 1.2) +- **Normalization**: 1/|o| sequence length normalization +- **Preset**: `make_gmpo(group_size=4)` uses same credit assignment as GRPO (`GroupNormalizedReturn`) + ## SFT / Offline RL Ludic supports supervised fine-tuning (SFT) and offline RL through the same abstractions: diff --git a/examples/code_exec/README.md b/examples/code_exec/README.md new file mode 100644 index 0000000..caa6f4e --- /dev/null +++ b/examples/code_exec/README.md @@ -0,0 +1,526 @@ +# Code Execution Training + +> Train LLMs on code generation with sandboxed test-driven evaluation. + +## What This Is + +This module provides an RL training environment where: +- The **agent generates code** in response to programming problems +- The **environment executes the code** in isolated containers +- **Test cases verify correctness** and provide reward signal +- The **trainer updates the policy** based on execution outcomes + +Key features: +- **Sandboxed execution** — Generated code runs in Docker/Podman containers for security +- **Persistent containers** — 40x faster than cold-start containers (17ms vs 700ms per execution) +- **Automatic caching** — Skip redundant executions (especially valuable with CISPO/GRPO) +- **Multi-backend** — Works on laptop (Docker) or HPC clusters (Podman-HPC) + +## Who This Is For + +**Experienced Ludic users**: Jump to [Quick Start](#quick-start) for copy-paste examples. + +**New to Ludic**: Read [How It Works](#how-it-works) first to understand the concepts. + +**Prerequisites**: +- Familiarity with Ludic's training concepts (`Trainer`, `RolloutEngine`, `BatchSource`) +- Docker running locally, or Podman-HPC on your HPC cluster +- A vLLM inference server for generation + +--- + +## Quick Start + +### Prerequisites + +1. **Docker daemon running** — See [Setup Guide](#setup-guide) if not +2. **HuggingFace token** — Create `.env` file: `echo 'HF_TOKEN=your_token' > .env` +3. **Dependencies**: `pip install docker datasets peft` + +### 5-Minute Local Run + +```bash +# Terminal 1: Start vLLM inference server +CUDA_VISIBLE_DEVICES=0 uv run --env-file .env python -m ludic.inference.vllm_server \ + --model Qwen/Qwen2.5-Coder-0.5B-Instruct + +# Terminal 2: Run training +CUDA_VISIBLE_DEVICES=1 PYTHONPATH=. uv run --env-file .env python examples/code_exec/train_apps.py \ + --model Qwen/Qwen2.5-Coder-0.5B-Instruct \ + --limit 100 \ + --train-steps 10 +``` + +You should see: +- Sandbox pool starting with 4 workers +- Baseline evaluation running +- Training steps with reward metrics + +### HPC Cluster Run (Slurm) + +```bash +# 1. Prepare environment on LOGIN NODE (one-time, requires internet) +./examples/code_exec/prepare_env.sh + +# 2. Submit job to compute nodes +sbatch examples/code_exec/train_apps_isambard.slurm +``` + +The Slurm script handles: +- Starting vLLM server on GPU 0 +- Running training on GPU 1 +- Auto-detecting Podman-HPC backend +- Structured logging in `logs/YYYY-MM-DD/` + +--- + +## How It Works + +### The Training Loop + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Training Loop │ +│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │ +│ │ Trainer │◄──│ BatchSource │◄──│ RolloutEngine │ │ +│ └─────────────┘ └──────────────┘ └────────┬────────┘ │ +│ ▲ │ │ +│ │ ┌───────────▼─────────┐ │ +│ Weight Updates │ SingleAgentProtocol │ │ +│ │ └────────────┬────────┘ │ +│ ▼ │ │ +│ ┌───────────┐ ┌────────────▼────────┐ │ +│ │ vLLM │◄────────────────────│ CodeExecEnv │ │ +│ │ Server │ generates code └────────────┬────────┘ │ +│ └───────────┘ │ │ +└─────────────────────────────────────────────────┼───────────┘ + │ executes + ┌────────────▼────────┐ + │ SandboxPool │ + │ ┌────┐ ┌────┐ │ + │ │ S1 │ │ S2 │ ... │ + │ └────┘ └────┘ │ + └─────────────────────┘ +``` + +**Step by step:** + +1. **RolloutEngine** creates a `CodeExecEnv` for each problem from the dataset +2. **Agent** (via vLLM) generates Python code given the problem prompt +3. **CodeExecEnv** sends the code to a sandboxed container for execution +4. **Test cases** are run against the code; results determine the reward +5. **Trainer** collects rollouts and updates the model weights +6. **Weights are pushed** back to vLLM for the next generation round + +### Key Concepts + +#### Sandboxing + +**Why sandbox?** LLM-generated code can be malicious, buggy, or resource-hungry. Sandboxing: +- Prevents file system access outside the container +- Limits memory and CPU usage +- Disables network access (by default) +- Isolates each execution from others + +**Persistent containers** are the key to performance. Instead of starting a new container per execution (700ms overhead), we keep containers running and reuse them (17ms overhead). + +#### Backend Auto-Detection + +| Environment | Priority | +|-------------|----------| +| Inside Slurm job | `podman-hpc` → `docker` | +| Outside Slurm | `docker` → `podman-hpc` | + +Override with `--sandbox-backend docker` or `--sandbox-backend podman-hpc`. + +#### Test-Driven Evaluation + +Each problem in the dataset has test cases (input/output pairs). The flow: + +1. **TestAdapter** extracts test cases from the dataset format (e.g., `APPSTestAdapter` for APPS) +2. **StdinStdoutRunner** executes the code with each test's input as stdin +3. **OutputVerifier** compares actual output to expected output +4. **Reward** is computed based on test pass rate + +#### Caching + +The LRU cache prevents redundant execution: + +- **Cache key**: `hash(code) + hash(tests)` +- **Hit rate**: Often 30-50% with CISPO/GRPO (multiple generations per prompt) +- **Speedup**: Cache hits return instantly (no container execution) + +Monitor cache performance: +```python +stats = pool.cache_stats +# {'hits': 150, 'misses': 50, 'size': 200} +hit_rate = stats['hits'] / (stats['hits'] + stats['misses']) +``` + +### Reward Shaping + +| Event | Reward | Configurable | Rationale | +|-------|--------|--------------|-----------| +| All tests pass | `+1.0` | — | Complete success | +| Some tests pass | `0.0` to `1.0` | `--partial-credit` | Smoother gradient signal | +| All tests fail | `0.0` | — | No partial credit by default | +| Compile error | `-0.1` | `compile_failure_reward` | Discourage syntax errors | +| Proper code block | `+0.05` | Parser reward | Encourage correct formatting | + +**When to enable partial credit:** +- Training from scratch (model needs incremental signal) +- Long test suites where all-or-nothing is too sparse + +**When to keep binary rewards:** +- Fine-tuning a capable model +- Problems where partial correctness is meaningless + +--- + +## Configuration Reference + +### Training Script Arguments (`train_apps.py`) + +#### Model & Inference + +| Flag | Default | Description | +|------|---------|-------------| +| `--model` | `Qwen/Qwen2.5-3B-Instruct` | Model name or path | +| `--host` | `127.0.0.1` | vLLM server host | +| `--port` | `8000` | vLLM server port | +| `--max-prompt-tokens` | `1024` | Max prompt length (longer prompts filtered) | +| `--max-new-tokens` | `4096` | Max generation length | + +#### Training + +| Flag | Default | Description | +|------|---------|-------------| +| `--train-steps` | `100` | Number of training steps | +| `--batch-size` | `4` | Rollout requests per batch | +| `--group-size` | `8` | CISPO group size (rollouts per prompt) | +| `--train-temperature` | `0.8` | Sampling temperature | +| `--max-seq-len` | `2048` | Max tokens per sample (truncation limit) | +| `--micro-token-budget` | `16384` | Max padded tokens per micro-batch | + +#### LoRA + +| Flag | Default | Description | +|------|---------|-------------| +| `--lora-rank` | `8` | LoRA rank | +| `--lora-alpha-mult` | `2.0` | Alpha = rank × mult | +| `--lora-dropout` | `0.0` | LoRA dropout | + +#### Dataset + +| Flag | Default | Description | +|------|---------|-------------| +| `--split` | `train` | Dataset split | +| `--limit` | None | Max samples to load | +| `--difficulty` | None | Filter: `introductory`, `interview`, `competition` | +| `--eval-samples` | `200` | Hold out for evaluation | + +#### Sandbox + +| Flag | Default | Description | +|------|---------|-------------| +| `--sandbox-backend` | `auto` | `auto`, `docker`, `podman-hpc` | +| `--sandbox-workers` | `4` | Container pool size | +| `--python-version` | `3.11` | Python in sandbox | +| `--timeout-per-test` | `1.0` | Per-test timeout (seconds) | +| `--partial-credit` | `False` | Enable fractional rewards | +| `--minimal-sandbox` | `False` | Skip memory/network limits (HPC compat) | +| `--max-concurrent-ops` | `8` | Semaphore limit for Podman | + +#### Evaluation + +| Flag | Default | Description | +|------|---------|-------------| +| `--eval-every` | `25` | Eval every N steps | +| `--eval-before-start` | `True` | Run baseline evaluation | +| `--eval-concurrency` | `32` | Parallel eval rollouts | +| `--eval-temperature` | `0.0` | Greedy decoding for eval | + +#### Logging + +| Flag | Default | Description | +|------|---------|-------------| +| `--wandb` | `False` | Enable W&B logging | +| `--wandb-project` | `ludic-apps` | W&B project name | + +### Environment Configuration (`CodeExecConfig`) + +| Field | Default | Description | +|-------|---------|-------------| +| `timeout_per_test_s` | `5.0` | Per-test execution timeout | +| `memory_limit_mb` | `256` | Container memory limit | +| `max_tests` | `None` | Limit test count (None = all) | +| `stop_on_first_failure` | `True` | Early stop on failure | +| `compile_first` | `True` | Syntax check before running | +| `partial_credit` | `False` | Reward = pass_rate (vs binary) | +| `compile_failure_reward` | `-0.1` | Penalty for syntax errors | +| `use_cache` | `True` | Enable execution caching | + +### Sandbox Pool Sizing + +| Environment | CPUs | Recommended `--sandbox-workers` | +|-------------|------|--------------------------------| +| Laptop (M1/M2) | 8-10 | 4 | +| Workstation | 16-32 | 8-16 | +| HPC node | 64-128 | 24-64 | + +**Rule of thumb**: Each sandbox uses ~0.5-1 CPU core. Use `floor(cpus / 2)`. + +**Concurrency vs Workers**: +- `--concurrency` controls parallel rollouts (async tasks) +- `--sandbox-workers` controls parallel code executions +- If `concurrency > sandbox-workers`, tasks queue for sandboxes + +--- + +## End-to-End Example + +This complete example shows how to build a training script from scratch: + +```python +"""Minimal code execution training script.""" + +import asyncio +from datasets import load_dataset + +from ludic.agent import Agent +from ludic.context import FullDialog +from ludic.inference import VLLMChatClient, InferenceSpec, SamplingParams, ReturnSpec +from ludic.interaction import SingleAgentProtocol +from ludic.parsers import ParseResult +from ludic.distributed.adapters import create_vllm_publisher +from ludic.training import ( + RolloutEngine, RolloutBatchSource, Trainer, TrainerConfig, + make_cispo, make_dataset_queue_requests_fn, +) +from ludic.envs.code_exec import ( + CodeExecEnv, CodeExecConfig, create_sandbox_pool, APPSTestAdapter, +) + +async def main(): + # 1. Load dataset + ds = load_dataset("RoganInglis/apps-control-arena", split="train") + samples = [{"question": r["question"], "inputs": r["inputs"], "outputs": r["outputs"]} + for r in list(ds)[:100]] + + # 2. Create sandbox pool (shared across all envs) + pool = await create_sandbox_pool(n_workers=4, backend="auto") + + # 3. Setup inference client + client = VLLMChatClient(host="127.0.0.1", port=8000, enable_weight_updates=True) + publisher = create_vllm_publisher(client) + + # 4. Environment factory (captures pool via closure) + adapter = APPSTestAdapter() + env_config = CodeExecConfig(timeout_per_test_s=5.0, partial_credit=False) + + def env_factory(sample): + return CodeExecEnv(sample=sample, sandbox_pool=pool, + test_adapter=adapter, config=env_config) + + # 5. Protocol factory + def protocol_factory(): + return SingleAgentProtocol(agent=Agent( + client=client, model="Qwen/Qwen2.5-3B-Instruct", + ctx=FullDialog(), + parser=lambda raw: ParseResult(action=raw, reward=0.0, obs=None), + )) + + # 6. Setup training pipeline + engine = RolloutEngine( + env_registry={"apps": env_factory}, + protocol_registry={"single": protocol_factory}, + ) + + algo = make_cispo(group_size=8, clip_eps_high=5.0, length_normalize=True) + + batch_source = RolloutBatchSource( + orchestrator=engine, + credit_assigner=algo.credit_assigner, + requests_fn=make_dataset_queue_requests_fn(...), # See train_apps.py + concurrency=32, + ) + + # 7. Train + trainer = Trainer( + model=your_model, # Load with LoRA + algo=algo, + batch_source=batch_source, + publisher=publisher, + cfg=TrainerConfig(max_seq_len=2048, micro_token_budget=16384), + ) + + await trainer.train(num_steps=100) + + # 8. Cleanup + await pool.shutdown() + +if __name__ == "__main__": + asyncio.run(main()) +``` + +For a complete, production-ready script, see [`train_apps.py`](./train_apps.py). + +--- + +## Customization + +### Using a Different Dataset + +Implement the `TestAdapter` protocol: + +```python +from ludic.envs.code_exec import TestAdapter, TestCase + +class MyDatasetAdapter: + def get_tests(self, sample: dict) -> list[TestCase]: + return [ + TestCase(input=t["stdin"], expected=t["stdout"], id=f"test_{i}") + for i, t in enumerate(sample["tests"]) + ] + + def get_prompt(self, sample: dict) -> str: + return sample["problem_description"] + + def get_problem_id(self, sample: dict) -> str: + return sample["id"] +``` + +### Custom Reward Shaping + +Modify `CodeExecConfig`: + +```python +config = CodeExecConfig( + partial_credit=True, # Reward = fraction of tests passed + compile_failure_reward=-0.5, # Harsher penalty for syntax errors + stop_on_first_failure=False, # Run all tests for full feedback +) +``` + +### Custom Output Verification + +For floating-point comparisons: + +```python +from ludic.envs.code_exec.adapters import FloatTolerantVerifier + +verifier = FloatTolerantVerifier(abs_tol=1e-6, rel_tol=1e-6) +runner = StdinStdoutRunner(verifier=verifier) +``` + +For full API details, see the [Module README](../../src/ludic/envs/code_exec/README.md). + +--- + +## Troubleshooting + +### "Docker daemon not running" + +``` +docker.errors.DockerException: Error while fetching server API version +``` + +**Solution**: Start Docker Desktop (macOS/Windows) or `sudo systemctl start docker` (Linux). + +### Tests timing out + +**Symptoms**: Many `TIMEOUT` results, slow training. + +**Diagnosis**: Check if problems have expensive test cases. + +**Solutions**: +- Increase timeout: `--timeout-per-test 10.0` +- Use batch execution (enabled by default) +- Reduce number of tests: Set `max_tests` in `CodeExecConfig` + +### GPU out of memory + +**Solutions**: +- Reduce `--batch-size` +- Reduce `--micro-token-budget` +- Enable gradient checkpointing (already on by default) + +### Slow sandbox initialization + +**Symptoms**: "Starting sandbox pool..." takes 30+ seconds. + +**Solutions**: +- Reduce `--sandbox-workers` for initial testing +- Pre-pull images: `docker pull python:3.11-slim` + +### Podman-HPC: Image not found on compute node + +**Cause**: Images must be migrated to shared storage. + +**Solution**: +```bash +podman-hpc pull python:3.11-slim # Auto-migrates +podman-hpc images # Verify R/O=true +``` + +### Network access denied on compute node + +**Cause**: HPC compute nodes often lack internet access. + +**Solution**: Run `prepare_env.sh` on the login node first to pre-stage all dependencies. + +--- + +## Setup Guide + +### Docker (Local Development) + +```bash +# Install (macOS) +brew install --cask docker + +# Install (Linux) +curl -fsSL https://get.docker.com | sh +sudo usermod -aG docker $USER && newgrp docker + +# Start daemon +open -a Docker # macOS +sudo systemctl start docker # Linux + +# Verify +docker info && pip install docker>=7.0.0 +``` + +For detailed setup, see [Docker documentation](https://docs.docker.com/get-docker/). + +### Podman-HPC (HPC Clusters) + +```bash +# Pull and migrate image to shared storage +podman-hpc pull python:3.11-slim + +# Verify migration (R/O should be 'true') +podman-hpc images + +# Test execution +srun -N 1 podman-hpc run --rm python:3.11-slim python -c "print('hello')" +``` + +For cluster-specific setup, consult your HPC documentation or [Podman-HPC docs](https://github.com/NERSC/podman-hpc). + +### Verifying Your Setup + +```bash +# Run integration tests +pytest tests/integration/test_code_exec_docker.py -v + +# If tests are skipped, Docker is not accessible +``` + +--- + +## See Also + +- **Module README**: [src/ludic/envs/code_exec/README.md](../../src/ludic/envs/code_exec/README.md) — API reference, protocols, internals +- **Migration Guide**: [MIGRATION.md](./MIGRATION.md) — Training API changes and migration steps +- **Training Script**: [train_apps.py](./train_apps.py) — Production-ready example diff --git a/examples/code_exec/train_apps.py b/examples/code_exec/train_apps.py new file mode 100644 index 0000000..bb7496a --- /dev/null +++ b/examples/code_exec/train_apps.py @@ -0,0 +1,771 @@ +""" +APPS code generation training scaffold using CodeExecEnv with LoRA. + +This wires together: + - HuggingFace datasets for APPS code samples + - CodeExecEnv with sandboxed execution (Docker or Podman-HPC) + - SingleAgentProtocol with async env support + - LoRA adapters via PEFT for efficient fine-tuning + - GRPO with optional KL regularization + - Baseline + periodic evaluation on held-out samples + - RichLiveLogger (terminal dashboard) or WandB (cloud logging) + +Requirements: + - Container runtime: Docker daemon OR Podman-HPC (auto-detected) + - pip install docker>=7.0.0 datasets peft (for Docker backend) + - GPU(s) for training (optional for rollout-only mode) + +Usage: + # Start vLLM server (in one terminal) + CUDA_VISIBLE_DEVICES=0 uv run python -m ludic.inference.vllm_server \\ + --model Qwen/Qwen2.5-3B-Instruct + + # Run training with terminal dashboard (default) + CUDA_VISIBLE_DEVICES=1 PYTHONPATH=. uv run python examples/code_exec/train_apps.py \\ + --model Qwen/Qwen2.5-3B-Instruct \\ + --limit 500 --eval-samples 200 --train-steps 100 --final-save + + # Run training with KL regularization + CUDA_VISIBLE_DEVICES=1 PYTHONPATH=. uv run python examples/code_exec/train_apps.py \\ + --model Qwen/Qwen2.5-3B-Instruct \\ + --limit 500 --eval-samples 200 --train-steps 100 \\ + --kl-coeff 0.01 --final-save + + # Run training with WandB logging + CUDA_VISIBLE_DEVICES=1 PYTHONPATH=. uv run python examples/code_exec/train_apps.py \\ + --model Qwen/Qwen2.5-3B-Instruct \\ + --limit 500 --eval-samples 200 --train-steps 100 \\ + --wandb --wandb-project ludic-apps --final-save + +Key Features: + - LoRA: rank=8, alpha=16, target_modules="all-linear" (configurable) + - Eval: Baseline before training, periodic eval every N steps + - Logging: Terminal sparkline dashboard or WandB cloud tracking + - KL regularization: Optional penalty to prevent policy drift + +See README.md for detailed setup instructions. +""" + +from __future__ import annotations + +import argparse +import asyncio +import os +import queue +from typing import Any, Dict, List + +import torch +from datasets import load_dataset # type: ignore +from transformers import AutoModelForCausalLM, AutoTokenizer +from peft import get_peft_model, LoraConfig, TaskType + +from ludic.agent import Agent +from ludic.context import FullDialog +from ludic.inference import VLLMChatClient, InferenceSpec, SamplingParams, ReturnSpec +from ludic.interaction import SingleAgentProtocol +from ludic.parsers import ParseResult +from ludic.distributed.adapters import create_vllm_publisher +from ludic.eval import EngineEvaluator +from ludic.training import ( + RolloutEngine, + RolloutBatchSource, + Trainer, + TrainerConfig, + CheckpointConfig, + make_dataset_queue_requests_fn, + RequestsExhausted, + RolloutRequest, + EnvSpec, + ProtocolSpec, + # Algorithm + make_cispo, +) +from ludic.training import Reducer, RichLiveLogger, default_reducers +from ludic.training.loggers import WandbLogger +from ludic.training.hardware import configure_flash_attention, log_hardware_info + +# Import CodeExecEnv components +from ludic.envs.code_exec import ( + CodeExecEnv, + CodeExecConfig, + create_sandbox_pool, + SandboxBackend, +) +from ludic.envs.code_exec.adapters.apps import APPSTestAdapter, APPS_SYSTEM_PROMPT + +import logging + +logging.basicConfig( + level=logging.DEBUG, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" +) + + +def code_block_parser(raw: str) -> ParseResult: + """ + Parse code from markdown code blocks or raw text. + + Accepts: + - ```python\n...\n``` + - ```\n...\n``` + - Raw code (if no code blocks found) + + Returns parsed code with small format reward for proper code blocks. + """ + import re + + # Try to extract from markdown code block + code_block_pattern = r"```(?:python)?\s*\n(.*?)(?:\n)?```" + match = re.search(code_block_pattern, raw, re.DOTALL) + + if match: + code = match.group(1).strip() + return ParseResult( + action=code, reward=0.05, obs=None + ) # Small bonus for proper formatting + + # Empty response if no code block found + return ParseResult(action=None, reward=-0.1, obs="Please provide Python code.") + + +def load_apps_samples( + split: str = "train", + limit: int | None = None, + difficulty: str | None = None, +) -> List[Dict[str, Any]]: + """ + Load APPS samples from HuggingFace datasets. + + Args: + split: Dataset split ("train" or "test") + limit: Maximum number of samples to load + difficulty: Filter by difficulty ("introductory", "interview", "competition") + + Returns: + List of sample dicts with question, inputs, outputs, etc. + """ + # Load from the control-arena version which has cleaner formatting + ds = load_dataset("RoganInglis/apps-control-arena", split=split) + + samples: List[Dict[str, Any]] = [] + for idx, row in enumerate(ds): + # Filter by difficulty if specified + if difficulty and row.get("difficulty") != difficulty: + continue + + # Skip nondeterministic problems (they require special handling) + if row.get("is_nondeterministic", False): + continue + + samples.append( + { + "problem_id": row.get("problem_id", str(idx)), + "question": row["question"], + "inputs": row.get("inputs", []), + "outputs": row.get("outputs", []), + "difficulty": row.get("difficulty", "unknown"), + } + ) + + if limit is not None and len(samples) >= limit: + break + + return samples + + +def main(): + parser = argparse.ArgumentParser( + description="Train on APPS code generation dataset with LoRA" + ) + + # Model and inference + parser.add_argument("--model", default="Qwen/Qwen2.5-3B-Instruct") + parser.add_argument("--host", default="127.0.0.1", help="vLLM server host") + parser.add_argument("--port", type=int, default=8000, help="vLLM server port") + parser.add_argument( + "--max-prompt-tokens", type=int, default=1024, help="Max prompt tokens" + ) + parser.add_argument( + "--max-new-tokens", type=int, default=4096, help="Max new tokens" + ) + parser.add_argument( + "--stop", + nargs="*", + default=None, + help="Stop sequences (e.g. --stop '```' '')", + ) + + # LoRA configuration + parser.add_argument("--lora-rank", type=int, default=8, help="LoRA rank") + parser.add_argument( + "--lora-alpha", type=int, default=32, help="LoRA alpha" + ) + parser.add_argument("--lora-dropout", type=float, default=0.0, help="LoRA dropout") + parser.add_argument("--lora-use-rslora", action="store_true", help="Use RSLora") + + # Attention configuration + parser.add_argument( + "--disable-flash-attn", + action="store_true", + help="Disable Flash Attention (fall back to SDPA)", + ) + + # KL regularization + parser.add_argument( + "--kl-coeff", + type=float, + default=0.0, + help="KL penalty coefficient (0 = disabled)", + ) + + # Data + parser.add_argument("--split", default="train", help="Dataset split") + parser.add_argument("--limit", type=int, default=None, help="Max samples to load") + parser.add_argument("--difficulty", default=None, help="Filter by difficulty") + + # Sandbox + parser.add_argument( + "--max-concurrent-ops", + type=int, + default=8, + help="Max concurrent sandbox operations (prevents deadlock in HPC environments)", + ) + parser.add_argument( + "--sandbox-workers", type=int, default=4, help="Number of sandbox containers" + ) + parser.add_argument( + "--sandbox-backend", + default="auto", + choices=["auto", "docker", "podman-hpc"], + help="Sandbox backend (default: auto-detect)", + ) + parser.add_argument( + "--python-version", default="3.11", help="Python version in sandbox" + ) + parser.add_argument( + "--minimal-sandbox", + action="store_true", + help="Use minimal sandbox config (no memory/network limits) for HPC compatibility", + ) + parser.add_argument( + "--timeout-per-test", type=float, default=2.0, help="Timeout per test (seconds)" + ) + + # Training + parser.add_argument("--lr", type=float, default=1e-5, help="Learning rate") + parser.add_argument( + "--concurrency", type=int, default=32, help="Rollout concurrency" + ) + parser.add_argument( + "--batch-size", type=int, default=4, help="Rollout requests per batch" + ) + parser.add_argument( + "--train-steps", + type=int, + default=100, + help="Training steps (0=run until exhausted)", + ) + parser.add_argument("--group-size", type=int, default=8, help="GRPO group size") + parser.add_argument( + "--train-temperature", type=float, default=0.8, help="Sampling temperature" + ) + parser.add_argument( + "--partial-credit", action="store_true", help="Enable partial credit rewards" + ) + parser.add_argument( + "--max-seq-len", + type=int, + default=2048, + help="Max tokens per sample (sequences are truncated to this)", + ) + parser.add_argument( + "--micro-token-budget", + type=int, + default=16384, + help="Max padded tokens per micro-batch (replaces grad_accum_steps)", + ) + + # Evaluation + parser.add_argument( + "--eval-samples", + type=int, + default=200, + help="Number of samples to hold out for eval", + ) + parser.add_argument( + "--eval-every", type=int, default=25, help="Eval every N training steps" + ) + parser.add_argument( + "--eval-before-start", + action="store_true", + default=True, + help="Run baseline eval", + ) + parser.add_argument( + "--eval-concurrency", type=int, default=32, help="Eval concurrency" + ) + parser.add_argument( + "--eval-temperature", + type=float, + default=0.5, + help="Eval sampling temperature", + ) + + # Logging + parser.add_argument( + "--wandb", action="store_true", help="Enable Weights & Biases logging" + ) + parser.add_argument( + "--wandb-project", type=str, default="ludic-apps", help="WandB project name" + ) + + # Checkpoints + parser.add_argument("--rollout-log", default="data/apps_train_rollouts.jsonl") + parser.add_argument("--checkpoint-dir", default="checkpoints_apps") + parser.add_argument("--checkpoint-every", type=int, default=25) + parser.add_argument( + "--final-save", action="store_true", help="Save final checkpoint after training" + ) + + args = parser.parse_args() + + # Warn about concurrency/pool mismatch + if args.concurrency > args.sandbox_workers: + print( + f"WARNING: concurrency ({args.concurrency}) > sandbox-workers ({args.sandbox_workers})" + ) + print( + f" This means {args.concurrency - args.sandbox_workers} tasks will wait for sandboxes." + ) + print( + f" Consider: --sandbox-workers={args.concurrency} OR --concurrency={args.sandbox_workers}" + ) + print() + + # Setup rollout log + rollout_log_path = os.path.abspath(args.rollout_log) + os.makedirs(os.path.dirname(rollout_log_path) or ".", exist_ok=True) + open(rollout_log_path, "a", encoding="utf-8").close() + + # Load tokenizer early (needed for prompt length filtering) + print(f"Loading tokenizer: {args.model}") + tokenizer = AutoTokenizer.from_pretrained(args.model) + if tokenizer.pad_token_id is None: + tokenizer.pad_token_id = tokenizer.eos_token_id + + # Load data and split into train/eval sets + print(f"Loading APPS samples (split={args.split}, limit={args.limit})...") + all_samples = load_apps_samples(args.split, args.limit, args.difficulty) + if not all_samples: + print("ERROR: No APPS samples loaded.") + return 1 + + # Filter out samples with prompts that exceed max_prompt_tokens + # This ensures max_new_tokens can fit within the model's context window + def prompt_fits(sample: Dict[str, Any]) -> bool: + messages = [ + {"role": "system", "content": APPS_SYSTEM_PROMPT}, + {"role": "user", "content": sample["question"]}, + ] + token_ids = tokenizer.apply_chat_template(messages, add_generation_prompt=True) + return len(token_ids) <= args.max_prompt_tokens + + pre_filter_count = len(all_samples) + all_samples = [s for s in all_samples if prompt_fits(s)] + filtered_count = pre_filter_count - len(all_samples) + if filtered_count > 0: + print( + f"Filtered {filtered_count} samples exceeding {args.max_prompt_tokens} prompt tokens." + ) + + if not all_samples: + print( + "ERROR: All samples filtered out by prompt length. Increase --max-prompt-tokens." + ) + return 1 + + # Split: last N samples for eval (deterministic, reproducible) + if args.eval_samples > 0 and len(all_samples) > args.eval_samples: + train_samples = all_samples[: -args.eval_samples] + eval_samples = all_samples[-args.eval_samples :] + else: + train_samples = all_samples + eval_samples = [] + + print(f"Loaded {len(all_samples)} total samples (after filtering).") + print(f" Train: {len(train_samples)} samples") + print(f" Eval: {len(eval_samples)} samples (held out)") + + samples_q: queue.Queue = queue.Queue() + for idx, s in enumerate(train_samples): + samples_q.put((idx, s)) + + # Load model with LoRA + print(f"Loading model: {args.model}") + + # Configure Flash Attention (auto-detects optimal implementation) + device = "cuda" if torch.cuda.is_available() else "cpu" + attn_impl = configure_flash_attention(device, disable_flash_attn=args.disable_flash_attn) + log_hardware_info() + print(f"Attention implementation: {attn_impl}") + + base_model = AutoModelForCausalLM.from_pretrained( + args.model, + torch_dtype=torch.bfloat16, + trust_remote_code=True, + attn_implementation=attn_impl, + ) + + # Apply LoRA adapter + lora_config = LoraConfig( + task_type=TaskType.CAUSAL_LM, + inference_mode=False, + r=args.lora_rank, + lora_alpha=args.lora_alpha, + lora_dropout=args.lora_dropout, + use_rslora=False, + bias="none", + # target_modules="all-linear", + target_modules=[ + "q_proj", # Attention: Query projection + "k_proj", # Attention: Key projection + "v_proj", # Attention: Value projection + "o_proj", # Attention: Output projection + "gate_proj", # MLP: Gating projection + "up_proj", # MLP: Up projection + "down_proj", # MLP: Down projection + ], + ) + model = get_peft_model(base_model, lora_config) + model.to(device) + model.print_trainable_parameters() + print( + f"Model loaded on {device} with LoRA (rank={args.lora_rank}, alpha={args.lora_alpha})." + ) + + # Setup sandbox pool + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + # Build backend kwargs (minimal mode skips memory/network limits for HPC compatibility) + backend_kwargs = {} + if args.minimal_sandbox: + backend_kwargs["memory_limit"] = None + backend_kwargs["network_disabled"] = False + + try: + sandbox_pool = loop.run_until_complete( + create_sandbox_pool( + n_workers=args.sandbox_workers, + backend=args.sandbox_backend, + python_version=args.python_version, + max_concurrent_ops=args.max_concurrent_ops, + cache_size=10000, + **backend_kwargs, + ) + ) + except RuntimeError as e: + print(f"ERROR: {e}") + return 1 + + # Create shared adapter and config + test_adapter = APPSTestAdapter() + env_config = CodeExecConfig( + timeout_per_test_s=args.timeout_per_test, + stop_on_first_failure=False, + compile_first=True, + partial_credit=args.partial_credit, + compile_failure_reward=-0.1, + use_cache=True, + ) + + # Shared client for inference + client = VLLMChatClient(host=args.host, port=args.port, enable_weight_updates=True) + publisher = create_vllm_publisher(client) + + # Environment factory (captures sandbox_pool via closure) + def env_factory(sample: Dict[str, Any]) -> CodeExecEnv: + return CodeExecEnv( + sample=sample, + sandbox_pool=sandbox_pool, + test_adapter=test_adapter, + config=env_config, + system_prompt=APPS_SYSTEM_PROMPT, + ) + + env_registry = {"apps": env_factory} + + def protocol_factory(): + return SingleAgentProtocol( + agent=Agent( + client=client, + model=args.model, + ctx=FullDialog(), + parser=code_block_parser, + ) + ) + + protocol_registry = {"single_agent": protocol_factory} + + # Algorithm (CISPO - better for reasoning tokens) + algo = make_cispo( + group_size=args.group_size, + group_normalize_adv=True, + clip_eps_high=0.2, + length_normalize=True, + kl_coeff=args.kl_coeff, + ) + print("Using CISPO algorithm (better for reasoning/self-correction tokens)") + print(f"KL coefficient: {args.kl_coeff}") + + # Engine + batch source + engine = RolloutEngine( + env_registry=env_registry, + protocol_registry=protocol_registry, + jsonl_path=rollout_log_path, + ) + + train_inference = InferenceSpec( + sampling=SamplingParams( + temperature=args.train_temperature, + max_tokens=args.max_new_tokens, + stop=args.stop, + ), + return_=ReturnSpec.for_rl(top_logprobs_k=1), + ) + + requests_fn = make_dataset_queue_requests_fn( + samples_q, + batch_size=args.batch_size, + env_kind="apps", + protocol_kind="single_agent", + inference=train_inference, + protocol_kwargs={}, + request_meta_fn=lambda idx, sample: { + "sample_index": idx, + "problem_id": sample.get("problem_id", idx), + "difficulty": sample.get("difficulty", "unknown"), + }, + env_seed_fn=lambda idx, _sample: idx, + sampling_seed_fn=lambda idx, _sample: idx, + group_size=args.group_size, + ) + + batch_source = RolloutBatchSource( + orchestrator=engine, + credit_assigner=algo.credit_assigner, + requests_fn=requests_fn, + max_steps=1, # Single-step env + concurrency=args.concurrency, + ) + + # Trainer config with eval settings + cfg = TrainerConfig( + model_device=device, + lr=args.lr, + max_seq_len=args.max_seq_len, + micro_token_budget=args.micro_token_budget, + max_grad_norm=0.1, + pad_token_id=tokenizer.pad_token_id, + eval_at_start=bool(args.eval_before_start and eval_samples), + eval_every_n_steps=( + args.eval_every + if args.eval_every and args.eval_every > 0 and eval_samples + else None + ), + eval_concurrency=args.eval_concurrency, + eval_max_steps=1, + ) + + checkpoint_cfg = CheckpointConfig( + output_dir=args.checkpoint_dir, + every_n_steps=args.checkpoint_every, + max_to_keep=2, + save_optimizer=True, + ) + + # Training reducers + reducers = { + "all_passed_rate": Reducer( + kind="count_true", + source="all_passed", + normalize_by="rollouts", + ), + "compile_fail_rate": Reducer( + kind="count_true", + source="compile_failed", + normalize_by="rollouts", + ), + "avg_pass_rate": Reducer( + kind="mean", + source="pass_rate", + ), + "parse_err_rate": Reducer( + kind="count_true", + source="parse_error", + normalize_by="samples", + ), + "total_completion_tokens": Reducer( + kind="sum", + source="completion_length", + ), + **default_reducers(), + } + + # Eval reducers (for held-out samples) + eval_reducers = { + "all_passed_rate": Reducer( + kind="count_true", + source="all_passed", + normalize_by="rollouts", + as_percent=True, + ), + "compile_fail_rate": Reducer( + kind="count_true", + source="compile_failed", + normalize_by="rollouts", + as_percent=True, + ), + "avg_pass_rate": Reducer( + kind="mean", + source="pass_rate", + ), + "parse_error_rate": Reducer( + kind="count_true", + source="parse_error", + normalize_by="samples", + as_percent=True, + ), + "avg_completion_tokens": Reducer( + kind="mean", + source="completion_length", + ), + } + + # Logging metrics to track + log_keys = [ + # Core training + "train/loss", + "train/avg_total_reward", + # APPS-specific + "train/all_passed_rate", + "train/compile_fail_rate", + "train/avg_pass_rate", + "train/parse_err_rate", + "train/avg_completion_length", + # Eval metrics + "eval/all_passed_rate", + "eval/compile_fail_rate", + "eval/avg_pass_rate", + "eval/parse_error_rate", + "eval/avg_completion_tokens", + # Counts + "train/target_rollouts", + "train/num_samples", + ] + + # Configure logger (WandB or RichLive terminal dashboard) + if args.wandb: + train_logger = WandbLogger(project=args.wandb_project, config=dict(vars(args))) + print(f"WandB logging enabled: project={args.wandb_project}") + else: + train_logger = RichLiveLogger( + keys=log_keys, + spark_key="avg_total_reward", + history=100, + precision=4, + ) + + # Create EngineEvaluator for eval set + eval_inference = InferenceSpec( + sampling=SamplingParams( + temperature=args.eval_temperature, + max_tokens=args.max_new_tokens, + stop=args.stop, + ), + return_=ReturnSpec.for_eval(return_token_ids=True), + ) + + evaluator = None + if eval_samples: + evaluator = EngineEvaluator( + engine=RolloutEngine( + env_registry=env_registry, protocol_registry=protocol_registry + ), + requests_fn=lambda: [ + RolloutRequest( + env=EnvSpec(kind="apps", kwargs={"sample": sample}), + protocol=ProtocolSpec(kind="single_agent"), + env_seed=idx, + sampling_seed=idx, + inference=eval_inference, + num_episodes=1, + meta={ + "eval_idx": idx, + "problem_id": sample.get("problem_id", idx), + "difficulty": sample.get("difficulty", "unknown"), + }, + ) + for idx, sample in enumerate(eval_samples) + ], + reducers=eval_reducers, + max_steps=1, + timeout_s=cfg.eval_timeout_s, + concurrency=cfg.eval_concurrency, + ) + print( + f"Eval configured: {len(eval_samples)} samples, every {args.eval_every} steps" + ) + + trainer = Trainer( + model=model, + algo=algo, + batch_source=batch_source, + publisher=publisher, + enable_gradient_checkpointing=True, + cfg=cfg, + checkpoint_config=checkpoint_cfg, + train_logger=train_logger, + reducers=reducers, + evaluator=evaluator, + ) + + print(f"\nStarting training for {args.train_steps} steps...") + print(f" Samples: {len(train_samples)}") + print(f" Batch size: {args.batch_size}") + print(f" Group size: {args.group_size}") + print(f" Concurrency: {args.concurrency}") + print(f" Sandbox workers: {args.sandbox_workers}") + print(f" Sandbox backend: {args.sandbox_backend}") + print() + + try: + loop.run_until_complete(trainer.train(args.train_steps)) + except RequestsExhausted: + print("Training samples exhausted; stopping.") + except KeyboardInterrupt: + print("\nTraining interrupted.") + finally: + # Cleanup sandbox pool + print("Shutting down sandbox pool...") + loop.run_until_complete(sandbox_pool.shutdown()) + loop.close() + + # Save final checkpoint if requested + if args.final_save: + try: + ckpt_path = trainer.save_checkpoint(metadata={"final": True}) + print(f"Final checkpoint saved: {ckpt_path}") + except RuntimeError: + pass # No checkpointer configured + + # Close WandB if used + if args.wandb: + train_logger.close() + print("WandB run finished.") + + print("Training complete.") + return 0 + + +if __name__ == "__main__": + exit(main()) diff --git a/examples/fsdp2_training/train_math_fsdp2.py b/examples/fsdp2_training/train_math_fsdp2.py index 8c8730f..23b9907 100644 --- a/examples/fsdp2_training/train_math_fsdp2.py +++ b/examples/fsdp2_training/train_math_fsdp2.py @@ -30,7 +30,7 @@ from ludic.agent import Agent from ludic.context import FullDialog from ludic.inference import VLLMChatClient, InferenceSpec, SamplingParams, ReturnSpec -from ludic.interaction import SingleAgentSyncProtocol +from ludic.interaction import SingleAgentProtocol from ludic.distributed import create_vllm_publisher from ludic.parsers import boxed_parser, extract_last_boxed_content from ludic.eval import EngineEvaluator @@ -301,7 +301,7 @@ def main() -> None: env_registry = {"math": lambda sample: MATHEnv(sample=sample, system_prompt=args.system_prompt)} def protocol_factory(): - return SingleAgentSyncProtocol( + return SingleAgentProtocol( agent=Agent( client=client, model=args.model, diff --git a/examples/gsm8k/train_gsm8k.py b/examples/gsm8k/train_gsm8k.py index 6b3c4bb..6d5cec3 100644 --- a/examples/gsm8k/train_gsm8k.py +++ b/examples/gsm8k/train_gsm8k.py @@ -4,7 +4,7 @@ This wires together: - HF datasets for GSM8K samples - single-sample QA envs (GSM8KEnv) - - SingleAgentSyncProtocol with a shared VLLMChatClient + - SingleAgentProtocol with a shared VLLMChatClient - RolloutBatchSource + MonteCarloReturn credit - Trainer with REINFORCE loss @@ -27,7 +27,7 @@ from ludic.agent import Agent from ludic.context import FullDialog from ludic.inference import VLLMChatClient, InferenceSpec, SamplingParams, ReturnSpec -from ludic.interaction import SingleAgentSyncProtocol +from ludic.interaction import SingleAgentProtocol from ludic.parsers import boxed_parser from ludic.distributed.adapters import create_vllm_publisher from ludic.eval import EngineEvaluator @@ -140,7 +140,7 @@ def main(): env_registry = {"gsm8k": lambda sample: GSM8KEnv(sample=sample, system_prompt=args.system_prompt)} def protocol_factory(): - return SingleAgentSyncProtocol( + return SingleAgentProtocol( agent=Agent( client=client, model=args.model, diff --git a/examples/pipeline_rl/run_actor.py b/examples/pipeline_rl/run_actor.py index 462a9b2..0a154db 100644 --- a/examples/pipeline_rl/run_actor.py +++ b/examples/pipeline_rl/run_actor.py @@ -14,7 +14,7 @@ RolloutRequest, make_reinforce, ) -from ludic.interaction import SingleAgentSyncProtocol +from ludic.interaction import SingleAgentProtocol # Env Import from environments.tic_tac_toe import TicTacToeEnv @@ -40,11 +40,11 @@ def create_engine(client: VLLMChatClient) -> RolloutEngine: training_prompt = base_prompt + "\n\nOutput your move as a single XML tag, e.g., A1." def create_protocol(): - return SingleAgentSyncProtocol( + return SingleAgentProtocol( agent=Agent( - client=client, - model=MODEL_NAME, - ctx=FullDialog(system_prompt=training_prompt), + client=client, + model=MODEL_NAME, + ctx=FullDialog(system_prompt=training_prompt), parser=xml_tag_parser("move") ), stop_on_parse_error=True, diff --git a/examples/rejection_sampling.py b/examples/rejection_sampling.py index e12a77d..a2b52c4 100644 --- a/examples/rejection_sampling.py +++ b/examples/rejection_sampling.py @@ -17,7 +17,7 @@ from ludic.agent import Agent from ludic.context import FullDialog from ludic.inference import VLLMChatClient, InferenceSpec, SamplingParams -from ludic.interaction import SingleAgentSyncProtocol +from ludic.interaction import SingleAgentProtocol from ludic.parsers import xml_tag_parser from ludic.training import RolloutEngine, EnvSpec, ProtocolSpec, RolloutRequest from ludic.types import Rollout @@ -78,7 +78,7 @@ async def generate_filtered_data(args: argparse.Namespace) -> None: prompt_text = build_system_prompt() def create_protocol(): - return SingleAgentSyncProtocol( + return SingleAgentProtocol( agent=Agent( client=client, model=args.model, diff --git a/examples/tic_tac_toe/generate_synth_data.py b/examples/tic_tac_toe/generate_synth_data.py index ed9f90a..e635eb6 100644 --- a/examples/tic_tac_toe/generate_synth_data.py +++ b/examples/tic_tac_toe/generate_synth_data.py @@ -46,8 +46,7 @@ def build_system_prompt() -> str: """Build system prompt matching train_tic_tac_toe.py""" base_prompt = TicTacToeEnv().suggested_sysprompt or "" return ( - base_prompt - + "\n\nThink through the board in .... " + base_prompt + "\n\nThink through the board in .... " "After , output exactly one XML tag of the form A1 and nothing else." ) @@ -160,7 +159,9 @@ def apply_prompt_format( if include_step: truncated_messages = _truncate_history_messages(full_messages, placeholder) - chat_messages = truncated_messages if truncate_history else list(full_messages) + chat_messages = ( + truncated_messages if truncate_history else list(full_messages) + ) prompt_text = _messages_to_prompt(chat_messages) @@ -225,7 +226,7 @@ async def generate_synth_data(args: argparse.Namespace) -> None: prompt_text = build_system_prompt() def create_protocol(): - return SingleAgentSyncProtocol( + return SingleAgentProtocol( agent=Agent( client=client, model=args.model, @@ -248,7 +249,9 @@ def create_protocol(): if args.min_completion_tokens > 0 or args.max_completion_tokens > 0: return_spec = ReturnSpec.for_eval(return_token_ids=True) inference = InferenceSpec( - sampling=SamplingParams(temperature=args.temperature, max_tokens=args.max_tokens), + sampling=SamplingParams( + temperature=args.temperature, max_tokens=args.max_tokens + ), return_=return_spec, ) @@ -286,7 +289,9 @@ def create_protocol(): results[res] += 1 else: results["other"] += 1 - print(f"Generated {total} rollouts: {results['win']} wins, {results['loss']} losses, {results['draw']} draws") + print( + f"Generated {total} rollouts: {results['win']} wins, {results['loss']} losses, {results['draw']} draws" + ) # Filter and transform out_path = Path(args.output) @@ -323,16 +328,24 @@ def create_protocol(): too_short = stats.get("too_short", 0) kept_steps = stats.get("kept_steps", 0) if missing_trace: - print(f"Skipped {missing_trace} steps missing token traces (enable return_token_ids).") + print( + f"Skipped {missing_trace} steps missing token traces (enable return_token_ids)." + ) if too_short: - print(f"Skipped {too_short} steps with completion length < {args.min_completion_tokens}.") + print( + f"Skipped {too_short} steps with completion length < {args.min_completion_tokens}." + ) if too_long: - print(f"Skipped {too_long} steps with completion length > {args.max_completion_tokens}.") + print( + f"Skipped {too_long} steps with completion length > {args.max_completion_tokens}." + ) print(f"Kept {kept_steps} steps after length filtering.") if dropped_empty: print(f"Skipped {dropped_empty} rollouts with no remaining steps.") if args.transform: - print(f" (transformed to TruncatedThinking format with placeholder: '{args.placeholder}')") + print( + f" (transformed to TruncatedThinking format with placeholder: '{args.placeholder}')" + ) def main(): @@ -345,8 +358,12 @@ def main(): parser.add_argument("--port", type=int, default=8000) # Generation - parser.add_argument("--episodes", type=int, default=5000, help="Total episodes to generate.") - parser.add_argument("--max-steps", type=int, default=5, help="Max steps per episode.") + parser.add_argument( + "--episodes", type=int, default=5000, help="Total episodes to generate." + ) + parser.add_argument( + "--max-steps", type=int, default=5, help="Max steps per episode." + ) parser.add_argument("--concurrency", type=int, default=32) parser.add_argument("--temperature", type=float, default=0.8) parser.add_argument("--max-tokens", type=int, default=250) @@ -364,16 +381,35 @@ def main(): ) # Transformation - parser.add_argument("--transform", action="store_true", default=True, - help="Truncate history to TruncatedThinking format (default: True)") - parser.add_argument("--no-transform", action="store_false", dest="transform", - help="Keep full assistant history in prompts") - parser.add_argument("--placeholder", default="[TRUNCATED]", - help="Placeholder for truncated thinking blocks") - parser.add_argument("--lean", action="store_true", default=True, - help="Drop heavy metadata to keep JSONL small (default: True)") - parser.add_argument("--no-lean", action="store_false", dest="lean", - help="Keep full step/meta fields") + parser.add_argument( + "--transform", + action="store_true", + default=True, + help="Truncate history to TruncatedThinking format (default: True)", + ) + parser.add_argument( + "--no-transform", + action="store_false", + dest="transform", + help="Keep full assistant history in prompts", + ) + parser.add_argument( + "--placeholder", + default="[TRUNCATED]", + help="Placeholder for truncated thinking blocks", + ) + parser.add_argument( + "--lean", + action="store_true", + default=True, + help="Drop heavy metadata to keep JSONL small (default: True)", + ) + parser.add_argument( + "--no-lean", + action="store_false", + dest="lean", + help="Keep full step/meta fields", + ) # Output parser.add_argument("--output", default="data/tictactoe_sft_train_data.jsonl") diff --git a/examples/tic_tac_toe/sft_tic_tac_toe.py b/examples/tic_tac_toe/sft_tic_tac_toe.py index 80c7eff..6461210 100644 --- a/examples/tic_tac_toe/sft_tic_tac_toe.py +++ b/examples/tic_tac_toe/sft_tic_tac_toe.py @@ -206,7 +206,7 @@ def main() -> None: model = AutoModelForCausalLM.from_pretrained( args.model, - torch_dtype=torch.bfloat16, + dtype=torch.bfloat16, device_map={"": "cpu"}, low_cpu_mem_usage=True, trust_remote_code=True, diff --git a/examples/tic_tac_toe/train_tic_tac_toe.py b/examples/tic_tac_toe/train_tic_tac_toe.py index ebe040b..55230ca 100644 --- a/examples/tic_tac_toe/train_tic_tac_toe.py +++ b/examples/tic_tac_toe/train_tic_tac_toe.py @@ -3,7 +3,7 @@ This wires together: - TicTacToeEnv single-agent episodes - - SingleAgentSyncProtocol with a shared VLLMChatClient + - SingleAgentProtocol with a shared VLLMChatClient - RolloutBatchSource + GroupNormalizedReturn credit - Trainer with REINFORCE loss - Optional periodic eval of win rate @@ -25,7 +25,7 @@ from ludic.agent import Agent from ludic.context import FullDialog, TruncatedThinkingContext from ludic.inference import VLLMChatClient, InferenceSpec, SamplingParams, ReturnSpec -from ludic.interaction import SingleAgentSyncProtocol +from ludic.interaction import SingleAgentProtocol from ludic.distributed.adapters import create_vllm_publisher from ludic.parsers import compose_parsers, think_prefix_parser, xml_tag_parser from ludic.eval import EngineEvaluator @@ -216,7 +216,7 @@ def protocol_factory(): ctx = TruncatedThinkingContext(system_prompt=system_prompt) else: ctx = FullDialog(system_prompt=system_prompt) - return SingleAgentSyncProtocol( + return SingleAgentProtocol( agent=Agent( client=client, model=args.model, diff --git a/pyproject.toml b/pyproject.toml index 274e1e7..a87fb8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,20 +11,27 @@ dependencies = [ "aiohttp>=3.13.2", "beartype>=0.22.9", "jaxtyping>=0.3.4", + "datasets>=4.4.2", "openai>=2.7.1", "peft>=0.18.0", "rich>=14.2.0", - "torch>=2.8.0", - "vllm>=0.13.0", + "setuptools>=79.0.1", + # CRITICAL: torch>=2.9.0 required for aarch64 CUDA wheels + # PyTorch 2.8.0 has NO aarch64 CUDA wheels - skip it! + # See: https://download.pytorch.org/whl/cu128/torch/ + "torch>=2.9.0", + # vLLM is Linux-only (depends on NVIDIA libraries) + # Use sys_platform marker to skip on macOS/Windows + "torch-c-dlpack-ext>=0.1.4", + "vllm>=0.12.0; sys_platform == 'linux'", + "wandb>=0.23.1", + # Flash Attention for efficient attention computation (Linux-only, requires CUDA) + "flash-attn>=2.7.0; sys_platform == 'linux'", ] [project.optional-dependencies] -pipelinerl = [ - "redis>=7.1.0", -] -examples = [ - "datasets==4.4.1", # pinned to the versions in uv.lock that are known to work - "math-verify==0.8.0", # pinned to the versions in uv.lock that are known to work +code-exec = [ + "docker>=7.1.0", ] [build-system] @@ -56,3 +63,55 @@ markers = [ "diagnostic: marks tests that primarily emit diagnostic reports rather than asserting strict correctness", ] testpaths = ["tests"] + +# ============================================================================= +# uv Configuration for Cross-Platform PyTorch +# ============================================================================= +# This configuration automatically selects the correct PyTorch wheels: +# - Linux: CUDA 12.8 wheels from pytorch-cu128 index +# - macOS/Windows: CPU wheels from pytorch-cpu index +# +# Usage: +# Local dev (macOS): uv sync +# HPC (Linux GPU): uv sync +# Linux CI (no GPU): uv sync --extra cpu +# +# See: https://docs.astral.sh/uv/guides/integration/pytorch/ +# See: https://docs.isambard.ac.uk/user-documentation/applications/ML-packages/ +# ============================================================================= + +[tool.uv] +# Flash Attention build configuration: +# - Disable build isolation so torch is available during compilation +# - Declare build-time dependencies explicitly +# - Set MAX_JOBS for parallel compilation +no-build-isolation-package = ["flash-attn"] + +[tool.uv.extra-build-dependencies] +flash-attn = ["torch", "packaging", "ninja"] + +[tool.uv.extra-build-variables] +flash-attn = { MAX_JOBS = "16" } + +# Platform-based torch source selection: +# - Linux: Use CUDA 12.8 wheels (supports both x86_64 and aarch64) +# - Non-Linux (macOS, Windows): Use CPU wheels +[tool.uv.sources] +torch = [ + { index = "pytorch-cpu", marker = "sys_platform != 'linux'" }, + { index = "pytorch-cu128", marker = "sys_platform == 'linux'" }, +] +torchvision = [ + { index = "pytorch-cpu", marker = "sys_platform != 'linux'" }, + { index = "pytorch-cu128", marker = "sys_platform == 'linux'" }, +] + +[[tool.uv.index]] +name = "pytorch-cpu" +url = "https://download.pytorch.org/whl/cpu" +explicit = true + +[[tool.uv.index]] +name = "pytorch-cu128" +url = "https://download.pytorch.org/whl/cu128" +explicit = true diff --git a/src/ludic/envs/code_exec/README.md b/src/ludic/envs/code_exec/README.md new file mode 100644 index 0000000..0c4453c --- /dev/null +++ b/src/ludic/envs/code_exec/README.md @@ -0,0 +1,263 @@ +# CodeExecEnv Module + +A sandboxed code execution environment for reinforcement learning on code generation tasks. + +## Module Structure + +``` +code_exec/ +├── __init__.py # Public API exports +├── types.py # Data types (TestCase, TestResult, BatchTestResult, etc.) +├── sandbox.py # Sandbox/SandboxPool protocols +├── docker_sandbox.py # Docker-based sandbox implementation + LRU cache +├── runners.py # Code execution strategies (StdinStdoutRunner) +├── env.py # CodeExecEnv (main RL environment) +└── adapters/ + ├── base.py # TestAdapter, OutputVerifier protocols + └── apps.py # APPS dataset adapter +``` + +## Core Abstractions + +### Sandbox Protocol + +```python +class Sandbox(Protocol): + """Single sandboxed execution environment.""" + + async def execute( + self, + code: str, + stdin: str = "", + timeout_s: float = 5.0, + ) -> ExecutionResult: + """Execute code and return result.""" + ... +``` + +### SandboxPool Protocol + +```python +class SandboxPool(Protocol): + """Pool of reusable sandboxes with caching.""" + + async def checkout(self, timeout_s: float = 30.0) -> Sandbox: + """Get a sandbox from the pool.""" + ... + + async def release(self, sandbox: Sandbox) -> None: + """Return sandbox to pool.""" + ... + + def cache_get(self, code_hash: str, tests_hash: str) -> BatchTestResult | None: + """Check cache for previous results.""" + ... + + def cache_put(self, code_hash: str, tests_hash: str, result: BatchTestResult) -> None: + """Store result in cache.""" + ... +``` + +### TestAdapter Protocol + +```python +class TestAdapter(Protocol): + """Extracts test cases from dataset samples.""" + + def extract_tests(self, sample: dict[str, Any]) -> list[TestCase]: + """Extract test cases from a sample.""" + ... + + def format_problem(self, sample: dict[str, Any]) -> str: + """Format problem description for prompt.""" + ... +``` + +### CodeRunner Protocol + +```python +class CodeRunner(Protocol): + """Executes code against test cases.""" + + async def run_tests( + self, + code: str, + tests: list[TestCase], + sandbox: Sandbox, + config: CodeExecConfig, + ) -> BatchTestResult: + """Run all tests and return results.""" + ... +``` + +## Usage + +### Basic Setup + +```python +from ludic.envs.code_exec import ( + CodeExecEnv, + CodeExecConfig, + DockerSandboxPool, + DockerSandboxConfig, +) +from ludic.envs.code_exec.adapters.apps import APPSTestAdapter + +# Create sandbox pool +pool_config = DockerSandboxConfig( + python_version="3.11", + memory_limit="256m", + cpu_quota=50000, + network_disabled=True, +) +pool = DockerSandboxPool(n_workers=4, config=pool_config) +await pool.start() + +# Create environment +env_config = CodeExecConfig( + timeout_per_test_s=5.0, + stop_on_first_failure=True, + partial_credit=False, +) +env = CodeExecEnv( + sample={"question": "...", "inputs": [...], "outputs": [...]}, + sandbox_pool=pool, + test_adapter=APPSTestAdapter(), + config=env_config, +) + +# Run episode +obs, info = await env.env_reset() +outcome = await env.env_step("print(input())") + +# Cleanup +await pool.shutdown() +``` + +### With SingleAgentProtocol + +The protocol automatically detects async environments: + +```python +from ludic.interaction import SingleAgentProtocol +from ludic.agent import Agent + +protocol = SingleAgentProtocol(agent=agent) +rollouts = await protocol.run(env=env, max_steps=3) +``` + +## Configuration + +### CodeExecConfig + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `timeout_per_test_s` | `float` | `5.0` | Timeout per test case | +| `stop_on_first_failure` | `bool` | `True` | Stop after first failed test | +| `compile_first` | `bool` | `True` | Check syntax before running | +| `partial_credit` | `bool` | `False` | Reward based on pass fraction | +| `compile_failure_reward` | `float` | `-0.1` | Reward for syntax errors | +| `timeout_reward` | `float` | `-0.05` | Reward for timeout | +| `use_cache` | `bool` | `True` | Enable result caching | + +### DockerSandboxConfig + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `python_version` | `str` | `"3.11"` | Python version in container | +| `memory_limit` | `str` | `"256m"` | Container memory limit | +| `cpu_quota` | `int` | `50000` | CPU quota (50% of one core) | +| `network_disabled` | `bool` | `True` | Disable container networking | + +## Implementing Custom Adapters + +```python +from ludic.envs.code_exec import TestAdapter, TestCase, ExactMatchVerifier + +class MyDatasetAdapter(TestAdapter): + def __init__(self): + self._verifier = ExactMatchVerifier(strip=True, normalize_whitespace=True) + + def extract_tests(self, sample: dict) -> list[TestCase]: + tests = [] + for i, (inp, out) in enumerate(zip(sample["inputs"], sample["outputs"])): + tests.append(TestCase(input=inp, expected=out, id=f"test_{i}")) + return tests + + def format_problem(self, sample: dict) -> str: + return sample["problem_statement"] + + @property + def verifier(self) -> ExactMatchVerifier: + return self._verifier +``` + +## Result Types + +### TestResult + +```python +@dataclass +class TestResult: + test_case: TestCase + passed: bool + actual: str | None + execution: ExecutionResult + error_message: str | None = None +``` + +### BatchTestResult + +```python +@dataclass +class BatchTestResult: + results: list[TestResult] + code_hash: str + tests_hash: str + + @property + def passed_count(self) -> int: ... + + @property + def total_count(self) -> int: ... + + @property + def all_passed(self) -> bool: ... + + @property + def pass_rate(self) -> float: ... +``` + +## Caching + +The `DockerSandboxPool` includes an LRU cache to avoid re-executing identical code: + +```python +pool = DockerSandboxPool( + n_workers=4, + config=config, + cache_size=10000, # Max cached results +) + +# Check cache stats +print(pool.cache_stats) +# {'hits': 150, 'misses': 50, 'size': 200, 'max_size': 10000} +``` + +Cache keys are computed from: +- SHA256 hash of the code +- SHA256 hash of serialized test cases + +## Thread Safety + +- `LRUCache`: Thread-safe via `threading.Lock` +- `DockerSandboxPool`: Async-safe via `asyncio.Queue` +- `CodeExecEnv`: Not thread-safe (one instance per rollout) + +## Dependencies + +**Required:** +- `docker>=7.0.0` - Docker Python SDK + +**Optional (for specific adapters):** +- `datasets` - HuggingFace datasets for APPS diff --git a/src/ludic/envs/code_exec/__init__.py b/src/ludic/envs/code_exec/__init__.py new file mode 100644 index 0000000..2b4f816 --- /dev/null +++ b/src/ludic/envs/code_exec/__init__.py @@ -0,0 +1,126 @@ +""" +Code execution environment for RL on code generation tasks. + +This module provides: + - CodeExecEnv: Environment that executes code against test cases + - Sandbox protocols: Async sandboxed execution + - Test adapters: Dataset-specific test extraction + - Code runners: Execution strategies (stdin/stdout, function calls, etc.) + - Backend selection: Auto-detection and manual selection of sandbox backends + +Supported backends: + - Docker (requires docker package + daemon): pip install docker>=7.0.0 + - Podman-HPC (HPC clusters): requires podman-hpc CLI + - Singularity (planned): not yet implemented + +Usage: + # Recommended: use the factory with auto-detection + from ludic.envs.code_exec import create_sandbox_pool + + pool = await create_sandbox_pool(n_workers=4) # Auto-detects backend + pool = await create_sandbox_pool(n_workers=4, backend="podman-hpc") # Explicit + + # Or import specific implementations + from ludic.envs.code_exec import DockerSandboxPool # Docker + from ludic.envs.code_exec import PodmanHPCSandboxPool # Podman-HPC +""" + +from __future__ import annotations + +from .types import ( + CompileStatus, + RunStatus, + CompileResult, + ExecutionResult, + TestCase, + TestResult, + BatchTestResult, +) +from .sandbox import Sandbox, SandboxPool +from .adapters.base import TestAdapter, OutputVerifier, ExactMatchVerifier +from .runners import CodeRunner, StdinStdoutRunner, compute_hash, hash_tests +from .env import CodeExecConfig, CodeExecEnv + +# Backend detection and factory (always available) +from .backend import ( + SandboxBackend, + detect_available_backend, + is_docker_available, + is_podman_hpc_available, + is_singularity_available, + get_backend_info, +) +from .factory import create_sandbox_pool + +# Docker-related imports are optional (requires docker package) +try: + from .docker_sandbox import ( + DockerSandboxConfig, + DockerSandbox, + DockerSandboxPool, + LRUCache, + ) + _DOCKER_AVAILABLE = True +except ImportError: + _DOCKER_AVAILABLE = False + DockerSandboxConfig = None # type: ignore[misc, assignment] + DockerSandbox = None # type: ignore[misc, assignment] + DockerSandboxPool = None # type: ignore[misc, assignment] + LRUCache = None # type: ignore[misc, assignment] + +# Podman-HPC imports (always available - uses subprocess, no external package) +from .podman_sandbox import ( + PodmanConfig, + PodmanHPCSandbox, + PodmanHPCSandboxPool, + PodmanError, +) + +__all__ = [ + # Types + "CompileStatus", + "RunStatus", + "CompileResult", + "ExecutionResult", + "TestCase", + "TestResult", + "BatchTestResult", + # Protocols + "Sandbox", + "SandboxPool", + "TestAdapter", + "OutputVerifier", + "CodeRunner", + # Implementations + "ExactMatchVerifier", + "StdinStdoutRunner", + # Environment + "CodeExecConfig", + "CodeExecEnv", + # Utilities + "compute_hash", + "hash_tests", + # Backend detection + "SandboxBackend", + "detect_available_backend", + "is_docker_available", + "is_podman_hpc_available", + "is_singularity_available", + "get_backend_info", + # Factory + "create_sandbox_pool", + # Podman-HPC (always available) + "PodmanConfig", + "PodmanHPCSandbox", + "PodmanHPCSandboxPool", + "PodmanError", +] + +# Add Docker-related exports only if available +if _DOCKER_AVAILABLE: + __all__.extend([ + "DockerSandboxConfig", + "DockerSandbox", + "DockerSandboxPool", + "LRUCache", + ]) diff --git a/src/ludic/envs/code_exec/adapters/__init__.py b/src/ludic/envs/code_exec/adapters/__init__.py new file mode 100644 index 0000000..f73237b --- /dev/null +++ b/src/ludic/envs/code_exec/adapters/__init__.py @@ -0,0 +1,19 @@ +""" +Dataset adapters for code execution environments. + +Each adapter knows how to extract test cases and prompts from a specific +dataset format (APPS, HumanEval, LeetCode, etc.). +""" + +from __future__ import annotations + +from .apps import APPS_SYSTEM_PROMPT, APPSTestAdapter +from .base import ExactMatchVerifier, OutputVerifier, TestAdapter + +__all__ = [ + "TestAdapter", + "OutputVerifier", + "ExactMatchVerifier", + "APPSTestAdapter", + "APPS_SYSTEM_PROMPT", +] diff --git a/src/ludic/envs/code_exec/adapters/apps.py b/src/ludic/envs/code_exec/adapters/apps.py new file mode 100644 index 0000000..cff6bb9 --- /dev/null +++ b/src/ludic/envs/code_exec/adapters/apps.py @@ -0,0 +1,144 @@ +""" +APPS dataset adapter. + +Compatible with: + - codeparrot/apps + - RoganInglis/apps-control-arena + - Similar stdin/stdout format datasets +""" + +from __future__ import annotations + +import hashlib +from typing import Any, Dict, List + +from ..types import TestCase + + +APPS_SYSTEM_PROMPT = """You are an expert Python programmer solving competitive programming problems. + +Your solution will be tested against multiple test cases with different inputs. All tests must pass. + +CRITICAL REQUIREMENTS: +1. Read the problem specification carefully - understand input/output format, constraints, and edge cases +2. Write a complete, self-contained Python script +3. Read input using input() or sys.stdin +4. Print output using print() - match the exact format required +5. Your code must compile without errors and handle all test cases + +OUTPUT FORMAT (you MUST follow this exactly): + + +Brief analysis: +- Input/output format +- Key algorithm or approach +- Edge cases to handle + + + +```python +# Your complete solution here +``` + + +IMPORTANT: +- Keep concise - focus on problem understanding and approach +- Ensure your code compiles cleanly (no syntax errors) +- Match output format exactly (spacing, newlines, etc.) +- Test your logic within before writing +- Your solution will be executed against hidden test cases""" + + +class APPSTestAdapter: + """ + Test adapter for APPS-style datasets. + + Compatible with: + - codeparrot/apps + - RoganInglis/apps-control-arena + - Similar stdin/stdout datasets + + APPS format: + - question: problem description (string) + - inputs: list of stdin strings + - outputs: list of expected stdout strings + - problem_id: unique identifier + + Code is expected to be a Python script that reads from stdin + and writes to stdout. + """ + + def __init__( + self, + *, + question_key: str = "question", + inputs_key: str = "inputs", + outputs_key: str = "outputs", + problem_id_key: str = "problem_id", + ) -> None: + """ + Args: + question_key: Key for problem description + inputs_key: Key for test inputs list + outputs_key: Key for expected outputs list + problem_id_key: Key for problem identifier + """ + self._question_key = question_key + self._inputs_key = inputs_key + self._outputs_key = outputs_key + self._problem_id_key = problem_id_key + + def get_prompt(self, sample: Dict[str, Any]) -> str: + """Extract problem description from sample.""" + return str(sample[self._question_key]) + + def get_problem_id(self, sample: Dict[str, Any]) -> str: + """Extract problem identifier from sample.""" + return str(sample.get(self._problem_id_key, "unknown")) + + def get_tests(self, sample: Dict[str, Any]) -> List[TestCase]: + """ + Extract test cases from sample. + + Args: + sample: Dataset sample with inputs and outputs lists + + Returns: + List of TestCase objects for stdin/stdout testing + + Raises: + ValueError: If inputs and outputs lists have different lengths + """ + inputs = sample[self._inputs_key] + outputs = sample[self._outputs_key] + + if len(inputs) != len(outputs): + raise ValueError( + f"Mismatched test case counts: {len(inputs)} inputs, " + f"{len(outputs)} outputs" + ) + + return [ + TestCase(input=inp, expected=out, id=f"test_{i}") + for i, (inp, out) in enumerate(zip(inputs, outputs)) + ] + + def hash_tests(self, tests: List[TestCase]) -> str: + """ + Compute stable hash of test cases for caching. + + Args: + tests: List of test cases to hash + + Returns: + 16-character hex hash + """ + # Create canonical representation + canonical = [(t.input, t.expected) for t in tests] + canonical_str = str(canonical) + + # Hash with SHA256 + hash_obj = hashlib.sha256(canonical_str.encode("utf-8")) + + # Return first 16 hex characters + return hash_obj.hexdigest()[:16] diff --git a/src/ludic/envs/code_exec/adapters/base.py b/src/ludic/envs/code_exec/adapters/base.py new file mode 100644 index 0000000..99d63bf --- /dev/null +++ b/src/ludic/envs/code_exec/adapters/base.py @@ -0,0 +1,249 @@ +""" +Base protocols and default implementations for dataset adapters. +""" + +from __future__ import annotations + +from typing import Any, Dict, List, Optional, Protocol, Tuple, runtime_checkable + +from ..types import TestCase + + +@runtime_checkable +class TestAdapter(Protocol): + """ + Extracts test cases from a dataset sample. + + Each dataset format needs its own adapter to map from the sample + schema to the TestCase abstraction. This decouples the CodeExecEnv + from any specific dataset format. + + Implementations should be stateless and reusable across samples. + """ + + __test__ = False # Prevent pytest from collecting this as a test class + + def get_tests(self, sample: Dict[str, Any]) -> List[TestCase]: + """ + Extract test cases from a sample. + + Args: + sample: A single dataset sample (row) + + Returns: + List of TestCase objects ready for execution + """ + ... + + def get_prompt(self, sample: Dict[str, Any]) -> str: + """ + Extract the problem prompt/question from a sample. + + This is the text shown to the agent as the initial observation. + + Args: + sample: A single dataset sample (row) + + Returns: + The problem description string + """ + ... + + def get_problem_id(self, sample: Dict[str, Any]) -> str: + """ + Extract unique problem identifier. + + Used for logging, caching keys, and result tracking. + + Args: + sample: A single dataset sample (row) + + Returns: + Unique identifier string + """ + ... + + def hash_tests(self, tests: List[TestCase]) -> str: + """ + Compute a stable hash of test cases for caching. + + The hash should be deterministic and capture all test inputs + and expected outputs. Used as part of the cache key. + + Args: + tests: List of test cases to hash + + Returns: + Hex string hash (typically 16 chars) + """ + ... + + +@runtime_checkable +class OutputVerifier(Protocol): + """ + Compares actual output against expected output. + + Separated from TestAdapter because the same comparison logic + (e.g., float tolerance, whitespace normalization) often applies + across different dataset formats. + """ + + def verify(self, actual: str, expected: str) -> Tuple[bool, Optional[str]]: + """ + Compare actual output against expected. + + Args: + actual: The actual output from code execution + expected: The expected output from the test case + + Returns: + Tuple of (passed, details) where: + - passed: True if outputs match + - details: Explanation of mismatch if not passed, None otherwise + """ + ... + + +class ExactMatchVerifier: + """ + Exact string match after stripping whitespace. + + This is the default verifier and works for most competitive + programming style problems (APPS, Codeforces, etc.). + """ + + def __init__(self, *, strip: bool = True, case_sensitive: bool = True) -> None: + """ + Args: + strip: Whether to strip leading/trailing whitespace + case_sensitive: Whether comparison is case-sensitive + """ + self._strip = strip + self._case_sensitive = case_sensitive + + def verify(self, actual: str, expected: str) -> Tuple[bool, Optional[str]]: + """Compare actual vs expected with configured normalization.""" + a = actual.strip() if self._strip else actual + e = expected.strip() if self._strip else expected + + if not self._case_sensitive: + a = a.lower() + e = e.lower() + + if a == e: + return True, None + + # Provide useful diff info for debugging + details = self._generate_diff_details(a, e) + return False, details + + def _generate_diff_details(self, actual: str, expected: str) -> str: + """Generate a human-readable diff explanation.""" + # Length mismatch + if len(actual) != len(expected): + return ( + f"Length mismatch: got {len(actual)} chars, " + f"expected {len(expected)} chars" + ) + + # Find first difference + for i, (ca, ce) in enumerate(zip(actual, expected)): + if ca != ce: + # Show context around the difference + start = max(0, i - 10) + end = min(len(actual), i + 10) + actual_ctx = actual[start:end] + expected_ctx = expected[start:end] + return ( + f"First diff at position {i}: " + f"got {repr(ca)}, expected {repr(ce)}. " + f"Context: got '{actual_ctx}', expected '{expected_ctx}'" + ) + + return "Unknown difference (possibly trailing content)" + + +class WhitespaceNormalizedVerifier: + """ + Verifier that normalizes all whitespace before comparison. + + Useful for problems where output formatting (spaces, newlines) + may vary but content should be the same. + """ + + def verify(self, actual: str, expected: str) -> Tuple[bool, Optional[str]]: + """Compare after normalizing all whitespace to single spaces.""" + a = " ".join(actual.split()) + e = " ".join(expected.split()) + + if a == e: + return True, None + + return False, f"Mismatch after whitespace normalization: got '{a[:100]}...', expected '{e[:100]}...'" + + +class FloatTolerantVerifier: + """ + Verifier that handles floating point comparisons with tolerance. + + Useful for numerical problems where small floating point differences + are acceptable. + """ + + def __init__( + self, + *, + abs_tol: float = 1e-9, + rel_tol: float = 1e-9, + strip: bool = True, + ) -> None: + """ + Args: + abs_tol: Absolute tolerance for float comparison + rel_tol: Relative tolerance for float comparison + strip: Whether to strip whitespace + """ + self._abs_tol = abs_tol + self._rel_tol = rel_tol + self._strip = strip + + def verify(self, actual: str, expected: str) -> Tuple[bool, Optional[str]]: + """ + Compare outputs, using float tolerance where applicable. + + Splits output into tokens and compares each. If both tokens + parse as floats, uses tolerance comparison. Otherwise uses + exact string match. + """ + a = actual.strip() if self._strip else actual + e = expected.strip() if self._strip else expected + + a_tokens = a.split() + e_tokens = e.split() + + if len(a_tokens) != len(e_tokens): + return False, f"Token count mismatch: got {len(a_tokens)}, expected {len(e_tokens)}" + + for i, (at, et) in enumerate(zip(a_tokens, e_tokens)): + if not self._tokens_match(at, et): + return False, f"Mismatch at token {i}: got '{at}', expected '{et}'" + + return True, None + + def _tokens_match(self, actual: str, expected: str) -> bool: + """Check if two tokens match (with float tolerance if applicable).""" + # Try exact match first + if actual == expected: + return True + + # Try float comparison + try: + a_float = float(actual) + e_float = float(expected) + diff = abs(a_float - e_float) + threshold = max(self._abs_tol, self._rel_tol * abs(e_float)) + return diff <= threshold + except ValueError: + # Not floats, exact match already failed + return False diff --git a/src/ludic/envs/code_exec/backend.py b/src/ludic/envs/code_exec/backend.py new file mode 100644 index 0000000..c236b41 --- /dev/null +++ b/src/ludic/envs/code_exec/backend.py @@ -0,0 +1,171 @@ +""" +Sandbox backend detection and selection. + +This module provides: + - SandboxBackend: Enumeration of supported sandbox backends + - detect_available_backend(): Auto-detection based on environment + - is_*_available(): Individual backend availability checks + +Auto-detection priority: + - In Slurm job: podman-hpc → docker → error + - Outside Slurm: docker → podman-hpc → error + +Usage: + from ludic.envs.code_exec.backend import detect_available_backend, SandboxBackend + + # Auto-detect + backend = detect_available_backend() + + # Manual selection + if backend == SandboxBackend.PODMAN_HPC: + from ludic.envs.code_exec.podman_sandbox import PodmanHPCSandboxPool + pool = PodmanHPCSandboxPool(n_workers=4) +""" + +from __future__ import annotations + +import os +import shutil +from enum import Enum + + +class SandboxBackend(str, Enum): + """Supported sandbox backends.""" + + DOCKER = "docker" + PODMAN_HPC = "podman-hpc" + SINGULARITY = "singularity" + AUTO = "auto" + + +def detect_available_backend() -> str: + """ + Auto-detect the best available sandbox backend. + + Detection priority: + - In Slurm job (SLURM_JOB_ID set): + 1. podman-hpc (most common on HPC) + 2. docker (some HPC clusters have Docker) + 3. error + - Outside Slurm: + 1. docker (most common for local development) + 2. podman-hpc + 3. error + + Returns: + Backend identifier (one of SandboxBackend values, excluding AUTO) + + Raises: + RuntimeError: If no sandbox backend is available + """ + in_slurm = os.environ.get("SLURM_JOB_ID") is not None + + if in_slurm: + # HPC environment: prefer podman-hpc + if is_podman_hpc_available(): + return SandboxBackend.PODMAN_HPC.value + if is_docker_available(): + return SandboxBackend.DOCKER.value + else: + # Local/cloud environment: prefer Docker + if is_docker_available(): + return SandboxBackend.DOCKER.value + if is_podman_hpc_available(): + return SandboxBackend.PODMAN_HPC.value + + # Singularity is deferred but check for future use + if is_singularity_available(): + # NOTE: Singularity backend not yet implemented + pass + + raise RuntimeError( + "No sandbox backend available. Install one of:\n" + " - Docker (daemon-based): pip install docker && start Docker daemon\n" + " - Podman-HPC (daemonless): available on HPC clusters with podman-hpc\n" + "\n" + "For HPC clusters, ensure you're running within a Slurm job:\n" + " srun --pty bash\n" + " # or\n" + " sbatch your_script.sh" + ) + + +def is_docker_available() -> bool: + """ + Check if Docker daemon is running and accessible. + + Returns: + True if Docker is available and responding + """ + try: + import docker + client = docker.from_env() + client.ping() + client.close() + return True + except ImportError: + # docker package not installed + return False + except Exception: + # Docker daemon not running or not accessible + return False + + +def is_podman_hpc_available() -> bool: + """ + Check if podman-hpc CLI is available. + + Note: This only checks if the command exists, not if containers + can actually be run (which may require being in a Slurm job). + + Returns: + True if podman-hpc command is in PATH + """ + return shutil.which("podman-hpc") is not None + + +def is_singularity_available() -> bool: + """ + Check if Singularity/Apptainer CLI is available. + + Returns: + True if singularity or apptainer command is in PATH + """ + return ( + shutil.which("singularity") is not None + or shutil.which("apptainer") is not None + ) + + +def get_backend_info() -> dict: + """ + Get information about all backend availability. + + Useful for debugging and status reporting. + + Returns: + Dict with backend names as keys and availability info as values + """ + in_slurm = os.environ.get("SLURM_JOB_ID") is not None + + return { + "environment": { + "in_slurm": in_slurm, + "slurm_job_id": os.environ.get("SLURM_JOB_ID"), + }, + "backends": { + SandboxBackend.DOCKER.value: { + "available": is_docker_available(), + "requires": "Docker daemon + docker package", + }, + SandboxBackend.PODMAN_HPC.value: { + "available": is_podman_hpc_available(), + "requires": "podman-hpc command (HPC clusters)", + }, + SandboxBackend.SINGULARITY.value: { + "available": is_singularity_available(), + "requires": "singularity/apptainer command", + "note": "Not yet implemented", + }, + }, + } diff --git a/src/ludic/envs/code_exec/batch_runner.py b/src/ludic/envs/code_exec/batch_runner.py new file mode 100644 index 0000000..ae0d998 --- /dev/null +++ b/src/ludic/envs/code_exec/batch_runner.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +""" +Batch test runner for ludic code execution sandbox. + +This script runs inside the container. It: +1. Reads manifest.json for test configuration +2. Optionally compiles the solution using py_compile +3. Runs tests in PARALLEL using multiprocessing.Pool (default 16 workers) +4. Outputs streaming JSONL results (one JSON object per line, flushed immediately) + +Usage: + python batch_runner.py [manifest_path] + +The manifest.json format: + { + "code_file": "solution.py", + "compile_first": true, + "timeout_s": 5.0, + "stop_on_first_failure": true, + "num_workers": 16, + "tests": [ + {"id": "test_0", "stdin": "5\\n", "expected": "25\\n"}, + {"id": "test_1", "stdin": "3\\n", "expected": "9\\n"} + ] + } + +Output format (streaming JSONL): + {"type": "compile", "status": "success", "duration_ms": 12.5} + {"type": "test", "id": "test_0", "status": "success", "stdout": "25\\n", ...} + {"type": "test", "id": "test_1", "status": "timeout", ...} + {"type": "done", "total_tests": 2, "passed": 1, "failed": 1, "compile_failed": false} + +Status values: + compile: success, syntax_error, timeout + test: success, runtime_error, timeout, memory_exceeded, not_run + +Note: This script is designed to be self-contained with no external dependencies +beyond Python's standard library. It will be bundled into the container at runtime. +""" + +from __future__ import annotations + +import json +import multiprocessing +import py_compile +import subprocess +import sys +import time +from typing import Any, Dict, Iterator, List, Optional, Tuple + + +def emit(obj: Dict[str, Any]) -> None: + """Write JSON line and flush immediately for streaming. + + Each line must be a complete JSON object to enable partial result + recovery if the container crashes mid-execution. + """ + print(json.dumps(obj), flush=True) + + +def compile_check(code_file: str, timeout_s: float) -> Dict[str, Any]: + """Run py_compile and return result dict. + + Args: + code_file: Path to the Python file to compile + timeout_s: Timeout for compilation (not currently enforced for py_compile) + + Returns: + Dict with type="compile" and status/error info + """ + start = time.perf_counter() + try: + py_compile.compile(code_file, doraise=True) + return { + "type": "compile", + "status": "success", + "duration_ms": (time.perf_counter() - start) * 1000, + } + except py_compile.PyCompileError as e: + # Extract line number from the exception + # PyCompileError has exc_value which contains the SyntaxError + error_line: Optional[int] = None + error_column: Optional[int] = None + error_message = str(e) + + # Try to extract line/column from the underlying SyntaxError + if hasattr(e, "exc_value") and e.exc_value is not None: + exc = e.exc_value + if hasattr(exc, "lineno"): + error_line = exc.lineno + if hasattr(exc, "offset"): + error_column = exc.offset + if hasattr(exc, "msg"): + error_message = exc.msg + + return { + "type": "compile", + "status": "syntax_error", + "error_message": error_message, + "error_line": error_line, + "error_column": error_column, + "duration_ms": (time.perf_counter() - start) * 1000, + } + + +def run_test(code_file: str, test: Dict[str, Any], timeout_s: float) -> Dict[str, Any]: + """Run a single test and return result dict. + + Args: + code_file: Path to the Python file to execute + test: Test specification with id, stdin, expected (optional) + timeout_s: Timeout in seconds for the test execution + + Returns: + Dict with type="test" and execution results + """ + start = time.perf_counter() + test_id = test.get("id", "unknown") + stdin_data = test.get("stdin", "") + + try: + proc = subprocess.run( + [sys.executable, code_file], + input=stdin_data, + capture_output=True, + text=True, + timeout=timeout_s, + ) + duration_ms = (time.perf_counter() - start) * 1000 + + # Classify status based on return code + if proc.returncode == 0: + status = "success" + elif proc.returncode == 137: + # SIGKILL - typically OOM killer + status = "memory_exceeded" + elif proc.returncode == 143: + # SIGTERM + status = "killed" + else: + status = "runtime_error" + + return { + "type": "test", + "id": test_id, + "status": status, + "stdout": proc.stdout, + "stderr": proc.stderr, + "exit_code": proc.returncode, + "duration_ms": duration_ms, + } + + except subprocess.TimeoutExpired as e: + # Capture any partial output + stdout = e.stdout.decode("utf-8", errors="replace") if e.stdout else "" + stderr = e.stderr.decode("utf-8", errors="replace") if e.stderr else "" + + return { + "type": "test", + "id": test_id, + "status": "timeout", + "stdout": stdout, + "stderr": stderr, + "exit_code": None, + "duration_ms": timeout_s * 1000, + } + + except Exception as e: + # Catch any unexpected errors (e.g., file not found) + duration_ms = (time.perf_counter() - start) * 1000 + return { + "type": "test", + "id": test_id, + "status": "runtime_error", + "stdout": "", + "stderr": f"Execution error: {e}", + "exit_code": None, + "duration_ms": duration_ms, + } + + +def _run_test_wrapper(args: Tuple[int, Dict[str, Any], str, float]) -> Tuple[int, Dict[str, Any]]: + """Wrapper for multiprocessing - must be top-level function for pickling. + + Args: + args: Tuple of (test_index, test_dict, code_file, timeout_s) + + Returns: + Tuple of (test_index, result_dict) to preserve ordering info + """ + i, test, code_file, timeout_s = args + result = run_test(code_file, test, timeout_s) + return (i, result) + + +def run_tests_parallel( + code_file: str, + tests: List[Dict[str, Any]], + timeout_s: float, + num_workers: int = 16, +) -> Iterator[Dict[str, Any]]: + """Run tests in parallel using multiprocessing.Pool. + + Uses imap_unordered for streaming results as they complete (not waiting + for all tests). This dramatically reduces wall-clock time when tests + have varying execution times. + + Args: + code_file: Path to the Python file to execute + tests: List of test specifications + timeout_s: Timeout per test in seconds + num_workers: Number of parallel worker processes (default 16 for HPC) + + Yields: + Test result dicts as they complete (unordered) + """ + if not tests: + return + + # Prepare arguments for each test + args_list = [(i, test, code_file, timeout_s) for i, test in enumerate(tests)] + + # Use spawn context to avoid fork issues with subprocess-heavy workloads + # This is safer on HPC systems where fork can cause issues with MPI, CUDA, etc. + ctx = multiprocessing.get_context("spawn") + + with ctx.Pool(processes=min(num_workers, len(tests))) as pool: + # imap_unordered streams results as they complete + for _i, result in pool.imap_unordered(_run_test_wrapper, args_list): + yield result + + +def main() -> None: + """Main entry point for batch runner.""" + # Get manifest path from command line or use default + manifest_path = sys.argv[1] if len(sys.argv) > 1 else "manifest.json" + + # Load manifest + try: + with open(manifest_path) as f: + manifest = json.load(f) + except Exception as e: + emit({ + "type": "error", + "message": f"Failed to load manifest: {e}", + }) + emit({ + "type": "done", + "total_tests": 0, + "passed": 0, + "failed": 0, + "compile_failed": False, + }) + return + + # Extract configuration + code_file = manifest.get("code_file", "solution.py") + compile_first = manifest.get("compile_first", True) + timeout_s = manifest.get("timeout_s", 5.0) + stop_on_first_failure = manifest.get("stop_on_first_failure", True) + num_workers = manifest.get("num_workers", 16) # Configurable via manifest + tests: List[Dict[str, Any]] = manifest.get("tests", []) + + # Step 1: Compile check (optional) + if compile_first: + result = compile_check(code_file, timeout_s) + emit(result) + + if result["status"] != "success": + # Compilation failed - emit done and exit + emit({ + "type": "done", + "total_tests": len(tests), + "passed": 0, + "failed": 0, + "compile_failed": True, + }) + return + + # Step 2: Run tests in parallel + passed = 0 + failed = 0 + received_ids: set[str] = set() + + # Use parallel execution for better throughput on HPC + for result in run_tests_parallel(code_file, tests, timeout_s, num_workers): + emit(result) # Stream immediately as each test completes + received_ids.add(result.get("id", "unknown")) + + if result["status"] == "success": + passed += 1 + else: + failed += 1 + + if stop_on_first_failure: + # Early termination - emit remaining tests as "not_run" + # Note: with parallel execution, some tests may have already + # started but the pool will be terminated on context exit + break + + # Emit any tests that didn't run (due to early termination or errors) + for test in tests: + test_id = test.get("id", "unknown") + if test_id not in received_ids: + emit({ + "type": "test", + "id": test_id, + "status": "not_run", + "stdout": "", + "stderr": "", + "exit_code": None, + "duration_ms": 0, + }) + + # Step 3: Emit done marker + emit({ + "type": "done", + "total_tests": len(tests), + "passed": passed, + "failed": failed, + "compile_failed": False, + }) + + +if __name__ == "__main__": + main() diff --git a/src/ludic/envs/code_exec/cache.py b/src/ludic/envs/code_exec/cache.py new file mode 100644 index 0000000..a5275b6 --- /dev/null +++ b/src/ludic/envs/code_exec/cache.py @@ -0,0 +1,129 @@ +""" +Shared LRU cache for code execution results. + +Provides thread-safe caching of BatchTestResult keyed by (code_hash, tests_hash). +Used by both Docker and Podman sandbox pools to avoid redundant execution of +identical code/test combinations. +""" + +from __future__ import annotations + +import threading +from collections import OrderedDict +from typing import Dict, Optional + +from .types import BatchTestResult + + +class LRUCache: + """ + Thread-safe LRU cache for BatchTestResult. + + Uses OrderedDict for LRU semantics and threading.Lock for safety. + Suitable for use across multiple async tasks sharing the same pool. + + Args: + max_size: Maximum number of entries to cache. Oldest entries are + evicted when this limit is exceeded. + """ + + def __init__(self, max_size: int = 10000): + self._max_size = max_size + self._cache: OrderedDict[tuple[str, str], BatchTestResult] = OrderedDict() + self._lock = threading.Lock() + self._hits = 0 + self._misses = 0 + + def get( + self, + code_hash: str, + tests_hash: str, + ) -> Optional[BatchTestResult]: + """ + Get cached result. + + On hit, moves item to end (most recently used). + Thread-safe. + + Args: + code_hash: Hash of the code being executed. + tests_hash: Hash of the test cases. + + Returns: + Cached BatchTestResult if found, None otherwise. + """ + key = (code_hash, tests_hash) + with self._lock: + if key in self._cache: + # Move to end (most recently used) + self._cache.move_to_end(key) + self._hits += 1 + return self._cache[key] + else: + self._misses += 1 + return None + + def put( + self, + code_hash: str, + tests_hash: str, + result: BatchTestResult, + ) -> None: + """ + Cache a result. + + Evicts oldest item if cache is full. + Thread-safe. + + Args: + code_hash: Hash of the code being executed. + tests_hash: Hash of the test cases. + result: The test result to cache. + """ + key = (code_hash, tests_hash) + with self._lock: + if key in self._cache: + # Update existing entry and move to end + self._cache[key] = result + self._cache.move_to_end(key) + else: + # Add new entry + self._cache[key] = result + # Evict oldest if over limit + if len(self._cache) > self._max_size: + self._cache.popitem(last=False) # FIFO: remove oldest + + def clear(self) -> None: + """Clear all cached entries. Thread-safe.""" + with self._lock: + self._cache.clear() + # Note: We don't reset hit/miss counters on clear + + @property + def stats(self) -> Dict[str, int]: + """ + Get cache statistics (thread-safe). + + Returns: + Dict with keys: hits, misses, size, max_size + """ + with self._lock: + return { + "hits": self._hits, + "misses": self._misses, + "size": len(self._cache), + "max_size": self._max_size, + } + + @property + def hit_rate(self) -> float: + """ + Get cache hit rate as a float between 0 and 1. + + Returns 0.0 if no lookups have been performed. + """ + with self._lock: + total = self._hits + self._misses + if total == 0: + return 0.0 + return self._hits / total diff --git a/src/ludic/envs/code_exec/docker_sandbox.py b/src/ludic/envs/code_exec/docker_sandbox.py new file mode 100644 index 0000000..5b5f354 --- /dev/null +++ b/src/ludic/envs/code_exec/docker_sandbox.py @@ -0,0 +1,725 @@ +""" +Docker-based sandbox implementation for code execution. + +This module provides: + - DockerSandboxConfig: Configuration for Docker containers + - DockerSandbox: Async Docker container sandbox + - DockerSandboxPool: Pool of Docker sandboxes with caching + +Requires: docker>=7.0.0 +Install with: pip install 'ludic[code-exec]' +""" + +from __future__ import annotations + +import asyncio +import hashlib +import io +import json +import logging +import os +import re +import tarfile +import time +import uuid +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass, field +from typing import AsyncIterator, Dict, List, Optional, Union + +logger = logging.getLogger(__name__) + +try: + import docker + from docker.models.containers import Container +except ImportError as e: + raise ImportError( + "Docker is not installed. Install it with: pip install 'ludic[code-exec]'" + ) from e + +from .parsing import ( + get_batch_runner_script, + parse_batch_compile_result, + parse_batch_test_result, + parse_syntax_error, +) +from .pool import BaseSandboxPool +from .sandbox import Sandbox, SandboxPool +from .types import ( + BatchExecutionSpec, + BatchTestResult, + CompileResult, + CompileStatus, + ExecutionResult, + RunStatus, + TestCase, +) + + +@dataclass +class DockerSandboxConfig: + """Configuration for Docker-based sandboxes.""" + + python_version: str = "3.11" + base_image: Optional[str] = None + memory_limit: str = "256m" + cpu_quota: int = 50000 # 50% of one CPU (out of 100000) + network_disabled: bool = True + working_dir: str = "/workspace" + + @property + def image(self) -> str: + """Get Docker image name (auto-generated or explicit).""" + if self.base_image: + return self.base_image + return f"python:{self.python_version}-slim" + + +class DockerSandbox: + """ + Async Docker container sandbox for Python code execution. + + Uses ThreadPoolExecutor to make docker-py calls non-blocking. + Implements the Sandbox protocol with full async support. + """ + + def __init__( + self, + container: Container, + config: DockerSandboxConfig, + executor: ThreadPoolExecutor, + ): + self._container = container + self._config = config + self._executor = executor + self._memory_limit_warned = False + + @property + def python_version(self) -> str: + return self._config.python_version + + async def reset(self) -> None: + """Clear workspace directory.""" + + def _reset(): + # Remove all files in workspace + self._container.exec_run( + f"sh -c 'rm -rf {self._config.working_dir}/*'", + workdir=self._config.working_dir, + ) + + loop = asyncio.get_event_loop() + await loop.run_in_executor(self._executor, _reset) + + async def compile( + self, + code: str, + *, + timeout_s: float = 5.0, + ) -> CompileResult: + """ + Syntax-check code using py_compile. + + Returns rich error info including line and column numbers. + """ + start = time.perf_counter() + + def _compile(): + # Write code to temp file + self._write_file("_check.py", code) + + # Run py_compile + result = self._container.exec_run( + "python -m py_compile _check.py", + workdir=self._config.working_dir, + demux=True, + ) + return result + + loop = asyncio.get_event_loop() + try: + # Run with timeout + result = await asyncio.wait_for( + loop.run_in_executor(self._executor, _compile), + timeout=timeout_s, + ) + + duration_ms = (time.perf_counter() - start) * 1000 + + exit_code = result.exit_code + stdout, stderr = result.output + + if exit_code == 0: + return CompileResult( + status=CompileStatus.SUCCESS, + duration_ms=duration_ms, + ) + + # Parse error message + error_msg = (stderr or b"").decode("utf-8", errors="replace") + line, column, clean_msg = parse_syntax_error(error_msg) + + # Classify error type + status = CompileStatus.SYNTAX_ERROR + if "ImportError" in error_msg or "ModuleNotFoundError" in error_msg: + status = CompileStatus.IMPORT_ERROR + elif not clean_msg: + status = CompileStatus.UNKNOWN_ERROR + + return CompileResult( + status=status, + error_message=clean_msg or error_msg, + error_line=line, + error_column=column, + duration_ms=duration_ms, + ) + + except asyncio.TimeoutError: + duration_ms = (time.perf_counter() - start) * 1000 + return CompileResult( + status=CompileStatus.TIMEOUT, + error_message=f"Compilation timed out after {timeout_s}s", + duration_ms=duration_ms, + ) + + async def execute( + self, + code: str, + *, + stdin: str = "", + skip_compile: bool = False, + timeout_s: float = 10.0, + memory_limit_mb: Optional[int] = None, + env_vars: Optional[Dict[str, str]] = None, + ) -> ExecutionResult: + """ + Execute code with full resource isolation and rich metadata. + + Compiles first, then executes if compilation succeeds (unless skip_compile=True). + """ + # Log warning for memory_limit_mb if provided (only once per sandbox) + if memory_limit_mb is not None and not self._memory_limit_warned: + logger.warning( + "Per-execution memory limits are not supported by docker exec. " + "Container-level memory limit (%s) is enforced instead.", + self._config.memory_limit, + ) + self._memory_limit_warned = True + + # Step 1: Compile + if skip_compile: + compile_result = CompileResult(status=CompileStatus.SUCCESS) + else: + compile_result = await self.compile(code, timeout_s=timeout_s) + + total_start = time.perf_counter() + + if not compile_result.success: + # Return early with compilation failure + total_ms = (time.perf_counter() - total_start) * 1000 + return ExecutionResult( + compile_result=compile_result, + run_status=RunStatus.NOT_RUN, + compile_duration_ms=compile_result.duration_ms, + total_duration_ms=total_ms, + ) + + # Step 2: Execute + run_start = time.perf_counter() + + def _execute(): + # Generate unique execution ID to avoid race conditions + exec_id = uuid.uuid4().hex[:8] + exec_file = f"_exec_{exec_id}.py" + input_file = f"input_{exec_id}.txt" + + # Write code to file + self._write_file(exec_file, code) + + # Write stdin to file if provided + if stdin: + self._write_file(input_file, stdin) + # Build command with stdin redirection + cmd = f"python {self._config.working_dir}/{exec_file} < {self._config.working_dir}/{input_file}" + else: + # Build command without redirection + cmd = f"python {self._config.working_dir}/{exec_file}" + + # Prepare environment + environment = env_vars or {} + + # Run with resource limits + result = self._container.exec_run( + cmd, + workdir=self._config.working_dir, + demux=True, + environment=environment, + ) + + return result + + loop = asyncio.get_event_loop() + + try: + # Run with timeout + result = await asyncio.wait_for( + loop.run_in_executor(self._executor, _execute), + timeout=timeout_s, + ) + + run_ms = (time.perf_counter() - run_start) * 1000 + total_ms = (time.perf_counter() - total_start) * 1000 + + exit_code = result.exit_code + stdout, stderr = result.output + + stdout_str = (stdout or b"").decode("utf-8", errors="replace") + stderr_str = (stderr or b"").decode("utf-8", errors="replace") + + # Classify run status + if exit_code == 0: + run_status = RunStatus.SUCCESS + elif exit_code == 137: # SIGKILL (OOM) + run_status = RunStatus.MEMORY_EXCEEDED + elif exit_code == 143: # SIGTERM + run_status = RunStatus.KILLED + else: + run_status = RunStatus.RUNTIME_ERROR + + return ExecutionResult( + compile_result=compile_result, + run_status=run_status, + stdout=stdout_str, + stderr=stderr_str, + exit_code=exit_code, + compile_duration_ms=compile_result.duration_ms, + run_duration_ms=run_ms, + total_duration_ms=total_ms, + ) + + except asyncio.TimeoutError: + run_ms = (time.perf_counter() - run_start) * 1000 + total_ms = (time.perf_counter() - total_start) * 1000 + + # Try to kill the process + try: + await loop.run_in_executor( + self._executor, + lambda: self._container.exec_run("pkill -9 python"), + ) + except Exception: + pass # Best effort cleanup + + return ExecutionResult( + compile_result=compile_result, + run_status=RunStatus.TIMEOUT, + stderr=f"Execution timed out after {timeout_s}s", + compile_duration_ms=compile_result.duration_ms, + run_duration_ms=run_ms, + total_duration_ms=total_ms, + ) + + def _write_file(self, path: str, content: str) -> None: + """ + Write a file to the container using tarfile. + + Docker API doesn't have a direct "write file" method, + so we create a tar archive in memory and extract it. + """ + # Create tar archive in memory + tar_buffer = io.BytesIO() + tar = tarfile.open(fileobj=tar_buffer, mode="w") + + # Add file to archive + file_data = content.encode("utf-8") + tarinfo = tarfile.TarInfo(name=path) + tarinfo.size = len(file_data) + tarinfo.mtime = time.time() + tar.addfile(tarinfo, io.BytesIO(file_data)) + tar.close() + + # Extract to container + tar_buffer.seek(0) + self._container.put_archive(self._config.working_dir, tar_buffer) + + # ------------------------------------------------------------------------- + # Batch execution (reduces ThreadPoolExecutor calls from O(N) to O(1)) + # ------------------------------------------------------------------------- + + async def execute_batch( + self, + spec: BatchExecutionSpec, + ) -> AsyncIterator[Union[CompileResult, ExecutionResult]]: + """ + Execute all tests in a single batch with streaming results. + + This method reduces the number of ThreadPoolExecutor calls by: + 1. Bundling code, manifest, and runner into a single tar + 2. Executing the batch runner once, which runs all tests sequentially + 3. Streaming results back as JSONL + + Args: + spec: Batch execution specification with code, tests, and options + + Yields: + CompileResult (if compile_first=True), then ExecutionResult for each test + """ + batch_dir = "_batch" + batch_start = time.perf_counter() + loop = asyncio.get_event_loop() + + # Build manifest for the batch runner + manifest = { + "code_file": "solution.py", + "compile_first": spec.compile_first, + "timeout_s": spec.timeout_s, + "stop_on_first_failure": spec.stop_on_first_failure, + "tests": [ + {"id": t.id or f"test_{i}", "stdin": t.input, "expected": t.expected} + for i, t in enumerate(spec.tests) + ], + } + + # Build and write tar archive + tar_data = self._build_batch_tar( + manifest=manifest, + code=spec.code, + runner_script=get_batch_runner_script(), + batch_dir=batch_dir, + ) + + def _write_tar(): + tar_buffer = io.BytesIO(tar_data) + self._container.put_archive(self._config.working_dir, tar_buffer) + + await loop.run_in_executor(self._executor, _write_tar) + + # Execute batch runner and stream results + manifest_path = f"{self._config.working_dir}/{batch_dir}/manifest.json" + runner_path = f"{self._config.working_dir}/{batch_dir}/batch_runner.py" + + run_start = time.perf_counter() + received_done = False + received_test_ids: set[str] = set() + compile_result: Optional[CompileResult] = None + + def _execute(): + result = self._container.exec_run( + f"python {runner_path} {manifest_path}", + workdir=f"{self._config.working_dir}/{batch_dir}", + demux=True, + ) + return result + + try: + result = await asyncio.wait_for( + loop.run_in_executor(self._executor, _execute), + timeout=spec.timeout_s * len(spec.tests) + 10.0, # Extra buffer + ) + + stdout, stderr = result.output + stdout_str = (stdout or b"").decode("utf-8", errors="replace") + + # Parse JSONL output + for line in stdout_str.strip().split("\n"): + if not line: + continue + + try: + result_dict = json.loads(line) + except json.JSONDecodeError: + logger.warning(f"Invalid JSON from batch runner: {line}") + continue + + result_type = result_dict.get("type") + + if result_type == "compile": + compile_result = parse_batch_compile_result(result_dict) + yield compile_result + if not compile_result.success: + break + + elif result_type == "test": + test_id = result_dict.get("id", "unknown") + received_test_ids.add(test_id) + exec_result = parse_batch_test_result(result_dict, run_start) + yield exec_result + + elif result_type == "done": + received_done = True + break + + elif result_type == "error": + logger.error(f"Batch runner error: {result_dict.get('message')}") + + except asyncio.TimeoutError: + logger.warning(f"Batch execution timed out") + + except Exception as e: + logger.warning(f"Batch execution failed: {e}") + + # Handle missing tests + if not received_done and compile_result is None: + compile_result = CompileResult( + status=CompileStatus.UNKNOWN_ERROR, + error_message="Batch execution terminated unexpectedly", + duration_ms=(time.perf_counter() - batch_start) * 1000, + ) + yield compile_result + + if not received_done and (compile_result is None or compile_result.success): + for i, test in enumerate(spec.tests): + test_id = test.id or f"test_{i}" + if test_id not in received_test_ids: + run_ms = (time.perf_counter() - run_start) * 1000 + yield ExecutionResult( + compile_result=compile_result or CompileResult( + status=CompileStatus.SUCCESS + ), + run_status=RunStatus.SANDBOX_ERROR, + stdout="", + stderr="Batch execution terminated unexpectedly", + exit_code=None, + run_duration_ms=run_ms, + total_duration_ms=run_ms, + ) + + def _build_batch_tar( + self, + manifest: dict, + code: str, + runner_script: str, + batch_dir: str = "_batch", + ) -> bytes: + """Build tar archive containing batch execution files.""" + buf = io.BytesIO() + with tarfile.open(fileobj=buf, mode="w") as tar: + # Create directory entry first + dir_info = tarfile.TarInfo(name=batch_dir) + dir_info.type = tarfile.DIRTYPE + dir_info.mode = 0o755 + dir_info.mtime = int(time.time()) + tar.addfile(dir_info) + + # Add manifest.json + manifest_data = json.dumps(manifest, indent=2).encode("utf-8") + info = tarfile.TarInfo(name=f"{batch_dir}/manifest.json") + info.size = len(manifest_data) + info.mtime = int(time.time()) + tar.addfile(info, io.BytesIO(manifest_data)) + + # Add solution.py + code_data = code.encode("utf-8") + info = tarfile.TarInfo(name=f"{batch_dir}/solution.py") + info.size = len(code_data) + info.mtime = int(time.time()) + tar.addfile(info, io.BytesIO(code_data)) + + # Add batch_runner.py + runner_data = runner_script.encode("utf-8") + info = tarfile.TarInfo(name=f"{batch_dir}/batch_runner.py") + info.size = len(runner_data) + info.mtime = int(time.time()) + tar.addfile(info, io.BytesIO(runner_data)) + + buf.seek(0) + return buf.read() + + +class DockerSandboxPool(BaseSandboxPool[DockerSandbox]): + """ + Pool of Docker sandboxes with LRU caching. + + Manages container lifecycle, checkout/release, and execution caching. + Inherits background reset pattern from BaseSandboxPool. + """ + + def __init__( + self, + n_workers: int = 4, + config: Optional[DockerSandboxConfig] = None, + cache_size: int = 10000, + executor_threads: int = 8, + auto_replace_failed: bool = False, + max_consecutive_failures: int = 5, + max_concurrent_ops: int = 8, + ): + # Initialize base pool + super().__init__( + n_workers=n_workers, + cache_size=cache_size, + auto_replace_failed=auto_replace_failed, + max_consecutive_failures=max_consecutive_failures, + max_concurrent_ops=max_concurrent_ops, + ) + + # Docker-specific configuration + self._config = config or DockerSandboxConfig() + self._executor = ThreadPoolExecutor(max_workers=executor_threads) + self._docker_client: Optional[docker.DockerClient] = None + + @property + def python_version(self) -> str: + return self._config.python_version + + async def _create_sandboxes(self) -> list[DockerSandbox]: + """ + Create all Docker containers. + + Pulls the image if needed, creates containers with resource limits. + Called by base class start() method. + """ + loop = asyncio.get_event_loop() + + def _start(): + # Create Docker client + client = docker.from_env() + + # Pull image if not present + try: + client.images.get(self._config.image) + except docker.errors.ImageNotFound: + print(f"Pulling image {self._config.image}...") + client.images.pull(self._config.image) + + # Define function to create a single container + def create_container(i: int): + # Generate container name with PID for uniqueness + container_name = f"ludic-sandbox-{self._config.python_version}-{os.getpid()}-{i}" + + # Remove existing container if present + try: + old = client.containers.get(container_name) + old.remove(force=True) + except docker.errors.NotFound: + pass + + # Create container with resource limits + container = client.containers.create( + image=self._config.image, + name=container_name, + detach=True, + command="sleep infinity", # Keep container alive + mem_limit=self._config.memory_limit, + cpu_quota=self._config.cpu_quota, + cpu_period=100000, # Standard 100ms period + network_disabled=self._config.network_disabled, + working_dir=self._config.working_dir, + auto_remove=False, # We'll manage cleanup + ) + + # Start container + container.start() + + # Create sandbox wrapper + return DockerSandbox( + container=container, + config=self._config, + executor=self._executor, + ) + + # Parallelize container creation + with ThreadPoolExecutor(max_workers=self._n_workers) as pool: + sandboxes = list(pool.map(create_container, range(self._n_workers))) + + return client, sandboxes + + # Run container creation in executor + self._docker_client, sandboxes = await loop.run_in_executor( + self._executor, _start + ) + + return sandboxes + + async def _stop_sandbox(self, sandbox: DockerSandbox) -> None: + """ + Stop and remove a single Docker container. + + Called during shutdown and when replacing a failed sandbox. + Errors are logged but not raised. + """ + loop = asyncio.get_event_loop() + + def _stop(): + try: + sandbox._container.stop(timeout=2) + sandbox._container.remove(force=True) + except Exception as e: + print(f"Warning: Failed to remove container: {e}") + + await loop.run_in_executor(self._executor, _stop) + + async def _create_replacement_sandbox(self) -> Optional[DockerSandbox]: + """ + Create a single replacement Docker container. + + Called when a sandbox fails to reset and auto_replace_failed is True. + Returns None if container creation fails. + """ + loop = asyncio.get_event_loop() + + def _create(): + if self._docker_client is None: + return None + + try: + # Generate unique container name + import random + i = random.randint(10000, 99999) + container_name = f"ludic-sandbox-{self._config.python_version}-{os.getpid()}-{i}" + + # Remove existing container if present + try: + old = self._docker_client.containers.get(container_name) + old.remove(force=True) + except docker.errors.NotFound: + pass + + # Create container with resource limits + container = self._docker_client.containers.create( + image=self._config.image, + name=container_name, + detach=True, + command="sleep infinity", + mem_limit=self._config.memory_limit, + cpu_quota=self._config.cpu_quota, + cpu_period=100000, + network_disabled=self._config.network_disabled, + working_dir=self._config.working_dir, + auto_remove=False, + ) + + # Start container + container.start() + + # Create sandbox wrapper + return DockerSandbox( + container=container, + config=self._config, + executor=self._executor, + ) + except Exception: + return None + + return await loop.run_in_executor(self._executor, _create) + + async def shutdown(self) -> None: + """ + Tear down all containers and release resources. + + Waits for pending resets, stops containers, closes Docker client, + and shuts down executor. + """ + # Base shutdown handles pending resets and calls _stop_sandbox + await super().shutdown() + + # Docker-specific cleanup + loop = asyncio.get_event_loop() + + def _close_client(): + if self._docker_client: + self._docker_client.close() + + await loop.run_in_executor(self._executor, _close_client) + + # Shutdown executor + self._executor.shutdown(wait=True) diff --git a/src/ludic/envs/code_exec/env.py b/src/ludic/envs/code_exec/env.py new file mode 100644 index 0000000..1e0f190 --- /dev/null +++ b/src/ludic/envs/code_exec/env.py @@ -0,0 +1,452 @@ +""" +Main environment for code execution RL tasks. + +This environment bridges the world of RL agents and code execution sandboxes, +providing a clean SingleAgentEnv interface for training LLMs to write code. + +Key design decisions: + 1. env_reset and env_step are async to support async sandbox operations + 2. The interaction protocol (Phase 6) must detect and await these coroutines + 3. Caching is handled at the pool level but controllable via config + 4. Rich info dict includes all execution metadata for analysis/logging + +Note: env_reset and env_step are async methods. The interaction protocol +must detect this and await them. See Phase 6 integration. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple + +from ludic.envs.single_agent_env import SingleAgentEnv +from ludic.types import Info, Observation, StepOutcome + +from .adapters.base import ExactMatchVerifier, OutputVerifier, TestAdapter +from .runners import CodeRunner, StdinStdoutRunner, compute_hash, hash_tests +from .sandbox import SandboxPool +from .types import BatchTestResult, TestCase + + +@dataclass +class CodeExecConfig: + """Configuration for CodeExecEnv behavior.""" + + # Execution limits + timeout_per_test_s: float = 5.0 # efficiency-focused default + memory_limit_mb: int = 256 + max_tests: Optional[int] = None # limit number of tests + stop_on_first_failure: bool = True + compile_first: bool = True + + # Reward shaping + partial_credit: bool = False # reward = fraction passed + compile_failure_reward: float = -0.1 + + # Observations + include_stderr_in_obs: bool = True + max_error_length: int = 500 + + # Caching + use_cache: bool = True + + +class CodeExecEnv(SingleAgentEnv): + """ + Code execution environment for RL training. + + This environment: + - Takes a dataset sample containing a problem + test cases + - Extracts prompt and tests via a TestAdapter + - Executes submitted code in a Sandbox from a SandboxPool + - Verifies outputs using an OutputVerifier + - Computes rewards based on test results + - Returns rich info dicts for logging/analysis + + The environment is single-step by design: agent submits code once, + gets results, episode ends. For multi-step refinement, wrap this + in a meta-environment or use a ReAct-style agent with tool calling. + + Example usage: + ```python + pool = await create_sandbox_pool(size=4) + adapter = APPSAdapter() + + env = CodeExecEnv( + sample=dataset[0], + sandbox_pool=pool, + test_adapter=adapter, + config=CodeExecConfig(partial_credit=True), + ) + + obs, info = await env.env_reset() + outcome = await env.env_step(agent_code) + ``` + """ + + def __init__( + self, + sample: Dict[str, Any], + *, + sandbox_pool: SandboxPool, + test_adapter: TestAdapter, + code_runner: Optional[CodeRunner] = None, + verifier: Optional[OutputVerifier] = None, + config: Optional[CodeExecConfig] = None, + system_prompt: Optional[str] = None, + ) -> None: + """ + Initialize the code execution environment. + + Args: + sample: Dataset sample containing problem and tests + sandbox_pool: Shared pool of sandboxes for execution + test_adapter: Adapter to extract prompt/tests from sample + code_runner: Runner for executing code (default: StdinStdoutRunner) + verifier: Output verifier (default: ExactMatchVerifier) + config: Environment configuration (default: CodeExecConfig()) + system_prompt: Optional system prompt for the agent + """ + super().__init__() + + self._sample = sample + self._sandbox_pool = sandbox_pool + self._test_adapter = test_adapter + self._code_runner = code_runner or StdinStdoutRunner( + default_timeout_s=config.timeout_per_test_s if config else 5.0, + memory_limit_mb=config.memory_limit_mb if config else 256, + ) + self._verifier = verifier or ExactMatchVerifier() + self._config = config or CodeExecConfig() + self._system_prompt = system_prompt + + # Episode state (set during reset) + self._problem_id: Optional[str] = None + self._prompt: Optional[str] = None + self._tests: Optional[List[TestCase]] = None + self._tests_hash: Optional[str] = None + self._current_obs: Optional[Observation] = None + + @property + def suggested_sysprompt(self) -> Optional[str]: + """Return the configured system prompt.""" + return self._system_prompt + + async def env_reset( + self, *, seed: Optional[int] = None + ) -> Tuple[Observation, Info]: + """ + Reset the environment for a new episode. + + Extracts the problem prompt and test cases from the sample, + but does not checkout a sandbox yet (that happens on step). + + Args: + seed: Optional random seed (unused in this deterministic env) + + Returns: + Tuple of (prompt, info) where info contains problem metadata + """ + # Extract problem components via adapter + self._problem_id = self._test_adapter.get_problem_id(self._sample) + self._prompt = self._test_adapter.get_prompt(self._sample) + self._tests = self._test_adapter.get_tests(self._sample) + + # Handle case where no tests were extracted + if not self._tests: + error_msg = f"No tests extracted for problem {self._problem_id}" + self._current_obs = error_msg + return self._current_obs, { + "problem_id": self._problem_id, + "error": "no_tests_extracted", + } + + # Apply max_tests limit if configured + if self._config.max_tests is not None: + self._tests = self._tests[: self._config.max_tests] + + # Compute tests hash for caching + self._tests_hash = hash_tests(self._tests) + + # Set current observation to the prompt + self._current_obs = self._prompt + + # Build info dict with episode metadata + info: Info = { + "problem_id": self._problem_id, + "num_tests": len(self._tests), + "tests_hash": self._tests_hash, + "python_version": self._sandbox_pool.python_version, + } + + return self._current_obs, info + + async def env_step(self, action: str) -> StepOutcome: + """ + Execute submitted code and return results. + + This is the core of the environment: takes the agent's code, + runs it through the sandbox, computes rewards, and builds + rich observations and info dicts. + + Args: + action: The code submitted by the agent + + Returns: + StepOutcome with observation, reward, termination flags, and info + """ + # Sanity check: ensure reset was called + if self._tests is None or self._tests_hash is None: + error_obs = "Error: env_reset() must be called before env_step()" + return StepOutcome( + obs=error_obs, + reward=-1.0, + truncated=False, + terminated=True, + info={"error": "reset_not_called"}, + ) + + # Handle empty code submission + if not action.strip(): + error_obs = "Error: Empty code submission" + return StepOutcome( + obs=error_obs, + reward=self._config.compile_failure_reward, + truncated=False, + terminated=True, + info={"error": "empty_code"}, + ) + + # Compute code hash for caching + code = action.strip() + code_hash = compute_hash(code) + + # Check cache FIRST, before checkout + result: Optional[BatchTestResult] = None + cache_hit = False + + if self._config.use_cache: + result = self._sandbox_pool.get_cached(code_hash, self._tests_hash) + if result is not None: + cache_hit = True + + # Only checkout sandbox if cache miss + if result is None: + # Checkout sandbox from pool + sandbox = await self._sandbox_pool.checkout() + + try: + # Run tests via code runner + result = await self._code_runner.run_tests( + sandbox=sandbox, + code=code, + tests=self._tests, + verifier=self._verifier, + stop_on_first_failure=self._config.stop_on_first_failure, + compile_first=self._config.compile_first, + ) + + # Cache result if enabled + if self._config.use_cache: + self._sandbox_pool.put_cached(code_hash, self._tests_hash, result) + + finally: + # Always release sandbox back to pool + await self._sandbox_pool.release(sandbox) + + # Compute reward based on results + reward = self._compute_reward(result) + + # Build observation for agent + obs = self._build_observation(result) + self._current_obs = obs + + # Build rich info dict for logging/analysis + info = self._build_info(result, code_hash, cache_hit) + + # Episode ends after single step (single-shot code generation) + return StepOutcome( + obs=obs, + reward=reward, + truncated=False, + terminated=True, + info=info, + ) + + def env_current_obs(self) -> Observation: + """ + Return the current observation. + + Returns: + The current observation string + """ + if self._current_obs is None: + return "Error: No observation available (call env_reset first)" + return self._current_obs + + def _compute_reward(self, result: BatchTestResult) -> float: + """ + Compute reward from test results. + + Reward schemes: + - partial_credit=False: 1.0 if all passed, 0.0 otherwise + - partial_credit=True: fraction of tests passed (0.0 to 1.0) + - Compilation failures get compile_failure_reward + + Args: + result: Batch test results + + Returns: + Scalar reward value + """ + # Compilation failure gets special penalty + if result.compile_failed: + return self._config.compile_failure_reward + + # All tests passed + if result.all_passed: + return 1.0 + + # Partial credit + if self._config.partial_credit: + return result.pass_rate + + # Binary reward (all or nothing) + return 0.0 + + def _build_observation(self, result: BatchTestResult) -> str: + """ + Build observation string from test results. + + The observation provides feedback to the agent about what went wrong, + including compilation errors, runtime errors, or test failures. + + Args: + result: Batch test results + + Returns: + Observation string for the agent + """ + # All tests passed - success message + if result.all_passed: + return ( + f"All {result.total_count} tests passed! " + f"Total execution time: {result.total_run_ms:.1f}ms" + ) + + # Compilation failed - show compile error + if result.compile_failed: + first = result.results[0] + compile_err = ( + first.execution.compile_result.error_message or "Unknown error" + ) + + # Truncate error if too long + if len(compile_err) > self._config.max_error_length: + compile_err = compile_err[: self._config.max_error_length] + "..." + + obs = f"Compilation failed: {compile_err}" + + if first.execution.compile_result.error_line is not None: + obs += f" (line {first.execution.compile_result.error_line})" + + return obs + + # Some tests failed - show first failure details + first_failure = result.first_failure + if first_failure is None: + # Should never happen, but handle gracefully + return f"Tests failed: {result.passed_count}/{result.total_count} passed" + + obs_parts = [f"Tests failed: {result.passed_count}/{result.total_count} passed"] + + # Add first failure details + if first_failure.comparison_details: + details = first_failure.comparison_details + if len(details) > self._config.max_error_length: + details = details[: self._config.max_error_length] + "..." + obs_parts.append(f"\nFirst failure: {details}") + + # Add stderr if configured and available + if self._config.include_stderr_in_obs and first_failure.execution.stderr: + stderr = first_failure.execution.stderr.strip() + if stderr: + if len(stderr) > self._config.max_error_length: + stderr = stderr[: self._config.max_error_length] + "..." + obs_parts.append(f"\nStderr: {stderr}") + + return "".join(obs_parts) + + def _build_info( + self, + result: BatchTestResult, + code_hash: str, + cache_hit: bool, + ) -> Info: + """ + Build rich info dict with all execution metadata. + + The info dict is JSON-serializable and includes everything needed + for logging, analysis, and debugging. + + Args: + result: Batch test results + code_hash: Hash of the submitted code + cache_hit: Whether result came from cache + + Returns: + Info dict with comprehensive metadata + """ + # Build per-test result summaries + test_results = [] + for test_result in result.results: + test_info = { + "test_id": test_result.test_case.id, + "passed": test_result.passed, + "compiled": test_result.compiled, + "ran": test_result.ran, + "run_status": ( + test_result.execution.run_status.value + if test_result.execution.run_status + else None + ), + "compile_status": test_result.execution.compile_result.status.value, + "run_duration_ms": test_result.execution.run_duration_ms, + "stdout": test_result.execution.stdout, + "stderr": test_result.execution.stderr, + } + + # Optionally include failure details + if not test_result.passed and test_result.comparison_details: + test_info["failure_reason"] = test_result.comparison_details + + test_results.append(test_info) + + # Build complete info dict + info: Info = { + # Problem metadata + "problem_id": self._problem_id, + "code_hash": code_hash, + "tests_hash": self._tests_hash, + # Test results summary + "passed": result.passed_count, + "total": result.total_count, + "all_passed": result.all_passed, + "pass_rate": result.pass_rate, + "compile_failed": result.compile_failed, + # Detailed test results + "test_results": test_results, + # Timing + "timing": { + "total_compile_ms": result.total_compile_ms, + "total_run_ms": result.total_run_ms, + "total_execution_ms": result.total_execution_ms, + }, + # Cache info + "cache_hit": cache_hit, + "cache_stats": self._sandbox_pool.cache_stats, + # Environment metadata + "python_version": self._sandbox_pool.python_version, + } + + return info diff --git a/src/ludic/envs/code_exec/factory.py b/src/ludic/envs/code_exec/factory.py new file mode 100644 index 0000000..d993dee --- /dev/null +++ b/src/ludic/envs/code_exec/factory.py @@ -0,0 +1,210 @@ +""" +Unified factory for creating sandbox pools. + +This module provides: + - create_sandbox_pool(): Async factory that auto-detects or uses specified backend + +Usage: + from ludic.envs.code_exec import create_sandbox_pool + + # Auto-detect backend + pool = await create_sandbox_pool(n_workers=4) + + # Explicit backend + pool = await create_sandbox_pool(n_workers=4, backend="podman-hpc") + + # With custom config + pool = await create_sandbox_pool( + n_workers=4, + backend="docker", + python_version="3.11", + memory_limit="512m", + ) +""" + +from __future__ import annotations + +from typing import Any, Optional + +from .backend import SandboxBackend, detect_available_backend +from .sandbox import SandboxPool + + +async def create_sandbox_pool( + n_workers: int = 4, + backend: str = "auto", + python_version: str = "3.11", + cache_size: int = 10000, + max_concurrent_ops: int = 8, + workspace_base_dir: str = "auto", + **backend_kwargs: Any, +) -> SandboxPool: + """ + Create and start a sandbox pool with the specified or auto-detected backend. + + This is the recommended way to create sandbox pools as it handles: + - Backend auto-detection based on environment + - Consistent configuration across backends + - Proper initialization (pull images, start containers) + + Args: + n_workers: Number of parallel sandboxes in the pool + backend: Backend to use ("auto", "docker", "podman-hpc", "singularity") + python_version: Python version for the sandbox containers + cache_size: Maximum number of cached execution results + max_concurrent_ops: Maximum concurrent sandbox operations (resets, exec + calls). Prevents deadlock in HPC environments. Default 8. + workspace_base_dir: Base directory for host-mounted workspaces. + - "auto": Auto-detect (use /local on HPC if SLURM_JOB_ID set) + - explicit path: Use this directory + - None: Disable bind mounts, use tar-based I/O + **backend_kwargs: Additional backend-specific configuration: + - memory_limit (str): Memory limit (e.g., "256m", "1g") + - cpu_quota (float): CPU limit as fraction (e.g., 0.5 = 50% of one CPU) + - network_disabled (bool): Disable network access (default: True) + - gpu (bool): Enable GPU access (podman-hpc only) + - image (str): Custom container image (overrides python_version) + - sif_path (str): Path to .sif file (singularity only) + + Returns: + Started SandboxPool instance + + Raises: + RuntimeError: If the specified backend is not available + ValueError: If an unknown backend is specified + + Examples: + # Auto-detect (recommended) + pool = await create_sandbox_pool(n_workers=4) + + # Docker with custom memory + pool = await create_sandbox_pool( + n_workers=4, + backend="docker", + memory_limit="512m", + ) + + # Podman-HPC with GPU + pool = await create_sandbox_pool( + n_workers=4, + backend="podman-hpc", + gpu=True, + ) + """ + # Resolve backend + if backend == "auto" or backend == SandboxBackend.AUTO: + resolved_backend = detect_available_backend() + print(f"Auto-detected sandbox backend: {resolved_backend}") + else: + resolved_backend = backend + + # Create pool based on backend + if resolved_backend == SandboxBackend.DOCKER.value: + pool = _create_docker_pool( + n_workers=n_workers, + python_version=python_version, + cache_size=cache_size, + max_concurrent_ops=max_concurrent_ops, + **backend_kwargs, + ) + + elif resolved_backend == SandboxBackend.PODMAN_HPC.value: + pool = _create_podman_hpc_pool( + n_workers=n_workers, + python_version=python_version, + cache_size=cache_size, + max_concurrent_ops=max_concurrent_ops, + workspace_base_dir=workspace_base_dir, + **backend_kwargs, + ) + + elif resolved_backend == SandboxBackend.SINGULARITY.value: + raise NotImplementedError( + "Singularity backend is not yet implemented. " + "Use 'docker' or 'podman-hpc' instead." + ) + + else: + raise ValueError( + f"Unknown backend: {resolved_backend}. " + f"Valid options: {', '.join(b.value for b in SandboxBackend if b != SandboxBackend.AUTO)}" + ) + + # Start the pool + await pool.start() + return pool + + +def _create_docker_pool( + n_workers: int, + python_version: str, + cache_size: int, + max_concurrent_ops: int = 8, + memory_limit: str = "256m", + cpu_quota: int = 50000, + network_disabled: bool = True, + image: Optional[str] = None, + **_kwargs: Any, +) -> SandboxPool: + """Create DockerSandboxPool with configuration.""" + try: + from .docker_sandbox import DockerSandboxConfig, DockerSandboxPool + except ImportError: + raise RuntimeError( + "Docker backend requires the docker package:\n" + " pip install docker>=7.0.0" + ) + + config = DockerSandboxConfig( + python_version=python_version, + base_image=image, + memory_limit=memory_limit, + cpu_quota=cpu_quota, + network_disabled=network_disabled, + ) + + return DockerSandboxPool( + n_workers=n_workers, + config=config, + cache_size=cache_size, + max_concurrent_ops=max_concurrent_ops, + ) + + +def _create_podman_hpc_pool( + n_workers: int, + python_version: str, + cache_size: int, + max_concurrent_ops: int = 8, + workspace_base_dir: str = "auto", + memory_limit: str = "256m", + cpu_quota: Optional[float] = None, + network_disabled: bool = True, + gpu: bool = False, + image: Optional[str] = None, + extra_args: Optional[list[str]] = None, + **_kwargs: Any, +) -> SandboxPool: + """Create PodmanHPCSandboxPool with configuration.""" + from .podman_sandbox import PodmanConfig, PodmanHPCSandboxPool + + config = PodmanConfig( + memory_limit=memory_limit, + cpu_quota=cpu_quota, + network_disabled=network_disabled, + gpu=gpu, + extra_args=extra_args, + ) + + # Determine image + if image is None: + image = f"python:{python_version}-slim" + + return PodmanHPCSandboxPool( + n_workers=n_workers, + image=image, + config=config, + cache_size=cache_size, + max_concurrent_ops=max_concurrent_ops, + workspace_base_dir=workspace_base_dir, + ) diff --git a/src/ludic/envs/code_exec/parsing.py b/src/ludic/envs/code_exec/parsing.py new file mode 100644 index 0000000..a24e6e3 --- /dev/null +++ b/src/ludic/envs/code_exec/parsing.py @@ -0,0 +1,127 @@ +"""Shared parsing utilities for code execution sandboxes.""" + +from __future__ import annotations + +import re +import time +from typing import Optional + +from .types import CompileResult, CompileStatus, ExecutionResult, RunStatus + +# Import batch runner script using importlib.resources +try: + from importlib.resources import files + + _BATCH_RUNNER_SCRIPT: Optional[str] = None + + def get_batch_runner_script() -> str: + """Lazy-load the batch runner script from package resources.""" + global _BATCH_RUNNER_SCRIPT + if _BATCH_RUNNER_SCRIPT is None: + _BATCH_RUNNER_SCRIPT = ( + files("ludic.envs.code_exec") + .joinpath("batch_runner.py") + .read_text() + ) + return _BATCH_RUNNER_SCRIPT + +except ImportError: + # Fallback for older Python versions + import pkg_resources + + def get_batch_runner_script() -> str: + return pkg_resources.resource_string( + "ludic.envs.code_exec", "batch_runner.py" + ).decode("utf-8") + + +def parse_syntax_error(error_msg: str) -> tuple[Optional[int], Optional[int], str]: + """Parse Python syntax error to extract line, column, and clean message.""" + line = None + column = None + clean_msg = "" + + # Try to find line number + line_match = re.search(r'line (\d+)', error_msg) + if line_match: + line = int(line_match.group(1)) + + # Try to find column number + col_match = re.search(r'column (\d+)', error_msg) + if col_match: + column = int(col_match.group(1)) + + # Extract error type and message + error_type_match = re.search( + r'(SyntaxError|IndentationError|TabError):\s*(.+)', error_msg + ) + if error_type_match: + error_type = error_type_match.group(1) + msg = error_type_match.group(2).strip() + clean_msg = f"{error_type}: {msg}" + else: + # Fall back to just extracting the last line + lines = [l.strip() for l in error_msg.split('\n') if l.strip()] + if lines: + clean_msg = lines[-1] + + return line, column, clean_msg + + +def parse_batch_compile_result(result: dict) -> CompileResult: + """Parse compile result from batch runner JSON.""" + status_str = result.get("status", "unknown_error") + + if status_str == "success": + status = CompileStatus.SUCCESS + elif status_str == "syntax_error": + status = CompileStatus.SYNTAX_ERROR + elif status_str == "timeout": + status = CompileStatus.TIMEOUT + else: + status = CompileStatus.UNKNOWN_ERROR + + return CompileResult( + status=status, + error_message=result.get("error_message"), + error_line=result.get("error_line"), + error_column=result.get("error_column"), + duration_ms=result.get("duration_ms", 0.0), + ) + + +def parse_batch_test_result( + result: dict, + run_start: float, +) -> ExecutionResult: + """Parse test result from batch runner JSON.""" + status_str = result.get("status", "runtime_error") + + if status_str == "success": + run_status = RunStatus.SUCCESS + elif status_str == "runtime_error": + run_status = RunStatus.RUNTIME_ERROR + elif status_str == "timeout": + run_status = RunStatus.TIMEOUT + elif status_str == "memory_exceeded": + run_status = RunStatus.MEMORY_EXCEEDED + elif status_str == "not_run": + run_status = RunStatus.NOT_RUN + elif status_str == "killed": + run_status = RunStatus.KILLED + else: + run_status = RunStatus.RUNTIME_ERROR + + duration_ms = result.get("duration_ms", 0.0) + total_ms = (time.perf_counter() - run_start) * 1000 + + return ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=run_status, + stdout=result.get("stdout", ""), + stderr=result.get("stderr", ""), + exit_code=result.get("exit_code"), + run_duration_ms=duration_ms, + total_duration_ms=total_ms, + cache_key=result.get("id", ""), # Pass test_id for matching in runner + ) diff --git a/src/ludic/envs/code_exec/podman_sandbox.py b/src/ludic/envs/code_exec/podman_sandbox.py new file mode 100644 index 0000000..e8b89e8 --- /dev/null +++ b/src/ludic/envs/code_exec/podman_sandbox.py @@ -0,0 +1,1031 @@ +""" +Podman-HPC sandbox implementation for code execution on HPC clusters. + +Provides: + - PodmanConfig: Configuration for Podman containers + - PodmanHPCSandbox: Async Podman container sandbox using subprocess + - PodmanHPCSandboxPool: Pool of Podman sandboxes with caching + +Podman-HPC is a daemonless container runtime wrapper for HPC clusters (e.g., Isambard). +Uses asyncio.create_subprocess_exec instead of docker-py SDK. + +**Important**: On some HPC systems (Isambard), podman-hpc's squashfs conversion +breaks the PATH variable. All commands in this module use absolute paths: + - /bin/sleep, /bin/mkdir, /bin/sh + - /usr/local/bin/python + - /usr/bin/pkill +""" + +from __future__ import annotations + +import asyncio +import io +import json +import logging +import math +import os +import re +import shutil +import tarfile +import time +import uuid +from dataclasses import dataclass +from pathlib import Path +from typing import AsyncIterator, Dict, List, Optional, Union + +from .parsing import ( + get_batch_runner_script, + parse_batch_compile_result, + parse_batch_test_result, + parse_syntax_error, +) +from .pool import BaseSandboxPool +from .sandbox import Sandbox, SandboxPool +from .types import ( + BatchExecutionSpec, + BatchTestResult, + CompileResult, + CompileStatus, + ExecutionResult, + RunStatus, + TestCase, +) + +logger = logging.getLogger(__name__) + + +@dataclass +class PodmanConfig: + """Configuration for Podman-HPC sandboxes.""" + + memory_limit: str = "256m" + cpu_quota: Optional[float] = None # CPU limit (e.g., 0.5 = 50% of one CPU) + network_disabled: bool = True + working_dir: str = "/workspace" + gpu: bool = False # Pass --gpu flag for GPU access + extra_args: Optional[list[str]] = None # Additional podman-hpc run args + + +def _get_container_name_prefix() -> str: + """ + Get container name prefix including SLURM_JOB_ID if in a Slurm job. + + Returns: + Container name prefix like "ludic-sandbox-12345" or "ludic-sandbox-local" + """ + slurm_job_id = os.environ.get("SLURM_JOB_ID") + if slurm_job_id: + return f"ludic-sandbox-{slurm_job_id}" + return "ludic-sandbox-local" + + +class PodmanHPCSandbox: + """ + Async Podman-HPC container sandbox for Python code execution. + + Uses persistent containers (sleep infinity) with exec for code execution. + All operations use asyncio.create_subprocess_exec for non-blocking I/O. + + Podman Concurrency Note: + Podman has known issues with concurrent operations (deadlock above ~8 + simultaneous exec calls). All sandboxes in a pool share an exec_semaphore + to prevent overwhelming podman's lock manager. + """ + + def __init__( + self, + container_name: str, + image: str, + config: PodmanConfig, + python_version: str = "3.11", + exec_semaphore: Optional[asyncio.Semaphore] = None, + workspace_host_dir: Optional[str] = None, + ): + self._container_name = container_name + self._image = image + self._config = config + self._python_version = python_version + self._exec_semaphore = exec_semaphore # Shared across all sandboxes in pool + self._workspace_host_dir = workspace_host_dir + self._started = False + + @property + def python_version(self) -> str: + return self._python_version + + async def start(self) -> None: + """Create and start the persistent container.""" + if self._started: + return + + # Remove existing container if present + await self._run_podman("rm", "-f", self._container_name, check=False) + + # Build run command + cmd = ["run", "-d", "--name", self._container_name] + + # Resource limits + if self._config.memory_limit: + cmd.extend(["--memory", self._config.memory_limit]) + if self._config.cpu_quota: + cmd.extend(["--cpus", str(self._config.cpu_quota)]) + if self._config.network_disabled: + cmd.extend(["--network", "none"]) + if self._config.gpu: + cmd.append("--gpu") + if self._config.extra_args: + cmd.extend(self._config.extra_args) + + # Add bind mount if workspace_host_dir is set + if self._workspace_host_dir: + logger.info( + f"[{self._container_name}] Using bind mount: " + f"{self._workspace_host_dir} -> {self._config.working_dir}" + ) + cmd.extend( + ["-v", f"{self._workspace_host_dir}:{self._config.working_dir}:rw"] + ) + + # Image and command (use full path for HPC compatibility) + cmd.extend([self._image, "/bin/sleep", "infinity"]) + + # Capture stderr to provide useful error messages + await self._run_podman(*cmd, capture=True) + + # Ensure workspace directory exists (use full path for HPC compatibility) + # Skip if using bind mount (host directory should already exist) + if not self._workspace_host_dir: + await self._run_podman( + "exec", + self._container_name, + "/bin/mkdir", + "-p", + self._config.working_dir, + capture=True, + ) + + self._started = True + + async def stop(self) -> None: + """Stop and remove the container.""" + if not self._started: + return + + await self._run_podman("stop", "-t", "2", self._container_name, check=False) + await self._run_podman("rm", "-f", self._container_name, check=False) + self._started = False + + async def reset(self) -> None: + """Clear workspace directory (in-place, no container restart).""" + if not self._started: + return + + if self._workspace_host_dir: + # Direct host filesystem cleanup - no podman exec, no semaphore + logger.debug( + f"[{self._container_name}] reset() using direct host cleanup..." + ) + start = time.perf_counter() + + workspace_path = Path(self._workspace_host_dir) + for item in workspace_path.iterdir(): + if item.is_dir(): + shutil.rmtree(item) + else: + item.unlink() + + elapsed = time.perf_counter() - start + logger.debug( + f"[{self._container_name}] reset() completed in {elapsed:.3f}s (direct)" + ) + return + + logger.debug(f"[{self._container_name}] reset() starting podman-hpc exec...") + start = time.perf_counter() + + await self._run_podman( + "exec", + self._container_name, + "/bin/sh", + "-c", + f"rm -rf {self._config.working_dir}/*", + ) + + elapsed = time.perf_counter() - start + logger.debug(f"[{self._container_name}] reset() completed in {elapsed:.3f}s") + + async def compile( + self, + code: str, + *, + timeout_s: float = 5.0, + ) -> CompileResult: + """Syntax-check code using py_compile.""" + start = time.perf_counter() + + try: + # Write code to container + await self._write_file("_check.py", code, timeout_s=timeout_s) + + # Run py_compile (use full path for HPC compatibility) + proc = await asyncio.wait_for( + self._run_podman( + "exec", + self._container_name, + "/usr/local/bin/python", + "-m", + "py_compile", + f"{self._config.working_dir}/_check.py", + check=False, + capture=True, + ), + timeout=timeout_s, + ) + + duration_ms = (time.perf_counter() - start) * 1000 + + if proc.returncode == 0: + return CompileResult( + status=CompileStatus.SUCCESS, + duration_ms=duration_ms, + ) + + # Parse error message + error_msg = proc.stderr or proc.stdout or "" + line, column, clean_msg = parse_syntax_error(error_msg) + + # Classify error type + status = CompileStatus.SYNTAX_ERROR + if "ImportError" in error_msg or "ModuleNotFoundError" in error_msg: + status = CompileStatus.IMPORT_ERROR + elif not clean_msg: + status = CompileStatus.UNKNOWN_ERROR + + return CompileResult( + status=status, + error_message=clean_msg or error_msg, + error_line=line, + error_column=column, + duration_ms=duration_ms, + ) + + except asyncio.TimeoutError: + duration_ms = (time.perf_counter() - start) * 1000 + return CompileResult( + status=CompileStatus.TIMEOUT, + error_message=f"Compilation timed out after {timeout_s}s", + duration_ms=duration_ms, + ) + + async def execute( + self, + code: str, + *, + stdin: str = "", + skip_compile: bool = False, + timeout_s: float = 10.0, + memory_limit_mb: Optional[int] = None, + env_vars: Optional[Dict[str, str]] = None, + ) -> ExecutionResult: + """Execute code with full resource isolation.""" + # Step 1: Compile + if skip_compile: + compile_result = CompileResult(status=CompileStatus.SUCCESS) + else: + compile_result = await self.compile(code, timeout_s=timeout_s) + + total_start = time.perf_counter() + + if not compile_result.success: + total_ms = (time.perf_counter() - total_start) * 1000 + return ExecutionResult( + compile_result=compile_result, + run_status=RunStatus.NOT_RUN, + compile_duration_ms=compile_result.duration_ms, + total_duration_ms=total_ms, + ) + + # Step 2: Execute + run_start = time.perf_counter() + + try: + # Generate unique filename to avoid race conditions + exec_id = uuid.uuid4().hex[:8] + exec_filename = f"_exec_{exec_id}.py" + + # Write code to container + await self._write_file(exec_filename, code, timeout_s=timeout_s) + + # Build exec command + exec_cmd = ["exec"] + if stdin: + exec_cmd.append("-i") + + # Add environment variables + if env_vars: + for key, val in env_vars.items(): + exec_cmd.extend(["-e", f"{key}={val}"]) + + exec_cmd.extend( + [ + self._container_name, + "/usr/local/bin/python", + f"{self._config.working_dir}/{exec_filename}", + ] + ) + + # Run with timeout + proc = await asyncio.wait_for( + self._run_podman( + *exec_cmd, + check=False, + capture=True, + input_data=stdin.encode("utf-8") if stdin else None, + ), + timeout=timeout_s, + ) + + run_ms = (time.perf_counter() - run_start) * 1000 + total_ms = (time.perf_counter() - total_start) * 1000 + + # Classify run status + exit_code = proc.returncode + if exit_code == 0: + run_status = RunStatus.SUCCESS + elif exit_code == 137: # SIGKILL (OOM) + run_status = RunStatus.MEMORY_EXCEEDED + elif exit_code == 143: # SIGTERM + run_status = RunStatus.KILLED + else: + run_status = RunStatus.RUNTIME_ERROR + + return ExecutionResult( + compile_result=compile_result, + run_status=run_status, + stdout=proc.stdout or "", + stderr=proc.stderr or "", + exit_code=exit_code, + compile_duration_ms=compile_result.duration_ms, + run_duration_ms=run_ms, + total_duration_ms=total_ms, + ) + + except asyncio.TimeoutError: + run_ms = (time.perf_counter() - run_start) * 1000 + total_ms = (time.perf_counter() - total_start) * 1000 + + # Best-effort cleanup - goes through exec_semaphore so won't deadlock + try: + await self._run_podman( + "exec", + self._container_name, + "/usr/bin/pkill", + "-9", + "python", + check=False, + capture=True, + ) + except Exception: + pass # Best effort, reset() will clean up anyway + + return ExecutionResult( + compile_result=compile_result, + run_status=RunStatus.TIMEOUT, + stderr=f"Execution timed out after {timeout_s}s", + compile_duration_ms=compile_result.duration_ms, + run_duration_ms=run_ms, + total_duration_ms=total_ms, + ) + + async def _write_file( + self, + filename: str, + content: str, + *, + timeout_s: float = 5.0, + ) -> None: + """ + Write a file to the container using tar pipe. + + Creates a tar archive in memory and pipes it to container. + This is more robust than echo for handling special characters. + """ + if self._workspace_host_dir: + # Direct host filesystem write - no podman exec, no semaphore + path = Path(self._workspace_host_dir) / filename + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + return + + # Create tar archive in memory + tar_buffer = io.BytesIO() + with tarfile.open(fileobj=tar_buffer, mode="w") as tar: + file_data = content.encode("utf-8") + tarinfo = tarfile.TarInfo(name=filename) + tarinfo.size = len(file_data) + tarinfo.mtime = int(time.time()) + tar.addfile(tarinfo, io.BytesIO(file_data)) + tar_buffer.seek(0) + + # Pipe tar to container + await asyncio.wait_for( + self._run_podman( + "exec", + "-i", + self._container_name, + "tar", + "-xC", + self._config.working_dir, + check=True, + capture=True, + input_data=tar_buffer.read(), + ), + timeout=timeout_s, + ) + + async def _run_podman( + self, + *args: str, + check: bool = True, + capture: bool = False, + input_data: Optional[bytes] = None, + ) -> "PodmanResult": + """ + Run a podman-hpc command asynchronously. + + For 'exec' commands, acquires the shared semaphore to prevent + overwhelming podman's lock manager (which deadlocks above ~8 + concurrent operations). + + Args: + *args: Command arguments (e.g., "exec", container_name, "python", ...) + check: Raise exception if command fails + capture: Capture stdout/stderr + input_data: Data to pipe to stdin + + Returns: + PodmanResult with returncode, stdout, stderr + """ + is_exec = args and args[0] == "exec" + + # Use semaphore for exec commands to prevent podman deadlock + if is_exec and self._exec_semaphore: + async with self._exec_semaphore: + return await self._run_podman_inner( + *args, check=check, capture=capture, input_data=input_data + ) + else: + return await self._run_podman_inner( + *args, check=check, capture=capture, input_data=input_data + ) + + async def _run_podman_inner( + self, + *args: str, + check: bool = True, + capture: bool = False, + input_data: Optional[bytes] = None, + ) -> "PodmanResult": + """Actually run the podman-hpc command (called by _run_podman).""" + start = time.perf_counter() + + proc = await asyncio.create_subprocess_exec( + "podman-hpc", + *args, + stdin=asyncio.subprocess.PIPE if input_data else None, + stdout=asyncio.subprocess.PIPE if capture else asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.PIPE if capture else asyncio.subprocess.DEVNULL, + ) + + stdout_bytes, stderr_bytes = await proc.communicate(input=input_data) + + elapsed = time.perf_counter() - start + if elapsed > 1.0: + cmd_preview = " ".join(args[:4]) + logger.warning( + f"[{self._container_name}] SLOW podman-hpc {cmd_preview}... " + f"took {elapsed:.2f}s" + ) + + result = PodmanResult( + returncode=proc.returncode or 0, + stdout=stdout_bytes.decode("utf-8", errors="replace") + if stdout_bytes + else "", + stderr=stderr_bytes.decode("utf-8", errors="replace") + if stderr_bytes + else "", + ) + + if check and result.returncode != 0: + raise PodmanError( + f"podman-hpc {' '.join(args)} failed with exit code {result.returncode}:\n" + f"{result.stderr}" + ) + + return result + + # ------------------------------------------------------------------------- + # Batch execution (reduces semaphore acquisitions from O(N) to O(1)) + # ------------------------------------------------------------------------- + + async def execute_batch( + self, + spec: BatchExecutionSpec, + ) -> AsyncIterator[Union[CompileResult, ExecutionResult]]: + """ + Execute all tests in a single batch with streaming results. + + This method reduces semaphore acquisitions from O(2N+1) to O(3) by: + 1. Bundling code, manifest, and runner into a single tar + 2. Executing the batch runner once, which runs all tests sequentially + 3. Streaming results back as JSONL + + Args: + spec: Batch execution specification with code, tests, and options + + Yields: + CompileResult (if compile_first=True), then ExecutionResult for each test + """ + batch_dir = "_batch" + batch_start = time.perf_counter() + + # Build manifest for the batch runner + manifest = { + "code_file": "solution.py", + "compile_first": spec.compile_first, + "timeout_s": spec.timeout_s, + "stop_on_first_failure": spec.stop_on_first_failure, + "tests": [ + {"id": t.id or f"test_{i}", "stdin": t.input, "expected": t.expected} + for i, t in enumerate(spec.tests) + ], + } + + # Build tar archive with all files + tar_data = self._build_batch_tar( + manifest=manifest, + code=spec.code, + runner_script=get_batch_runner_script(), + batch_dir=batch_dir, + ) + + # Write tar to container (1 semaphore acquisition) + await self._write_tar(tar_data, timeout_s=spec.timeout_s) + + # Execute batch runner and stream results (1 semaphore acquisition) + manifest_path = f"{self._config.working_dir}/{batch_dir}/manifest.json" + runner_path = f"{self._config.working_dir}/{batch_dir}/batch_runner.py" + + # Track timing and received results + run_start = time.perf_counter() + received_done = False + received_test_ids: set[str] = set() + compile_result: Optional[CompileResult] = None + + # Calculate aggregate timeout accounting for parallelization in batch_runner + # With N workers: timeout = (ceil(N_tests / workers) × timeout_per_test) + buffer + num_workers = 16 # Matches batch_runner.py default for HPC + parallel_batches = math.ceil(len(spec.tests) / num_workers) if spec.tests else 1 + aggregate_timeout = ( + spec.timeout_s * parallel_batches + 60.0 + ) # 60s buffer for HPC + + try: + async with self._exec_semaphore: + proc = await asyncio.create_subprocess_exec( + "podman-hpc", + "exec", + "--workdir", + f"{self._config.working_dir}/{batch_dir}", + self._container_name, + "python", + runner_path, + manifest_path, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + # Results collected from streaming to yield after timeout handling + streamed_results: list = [] + + async def _stream_results(): + """Stream results from batch runner, updating nonlocal state.""" + nonlocal received_done, compile_result + async for line_bytes in proc.stdout: + line = line_bytes.decode("utf-8", errors="replace").strip() + if not line: + continue + + try: + result = json.loads(line) + except json.JSONDecodeError: + logger.warning(f"Invalid JSON from batch runner: {line}") + continue + + result_type = result.get("type") + + if result_type == "compile": + compile_result = parse_batch_compile_result(result) + streamed_results.append(("compile", compile_result)) + if not compile_result.success: + # Compilation failed, we're done + break + + elif result_type == "test": + test_id = result.get("id", "unknown") + received_test_ids.add(test_id) + exec_result = parse_batch_test_result(result, run_start) + streamed_results.append(("test", exec_result)) + + elif result_type == "done": + received_done = True + break + + elif result_type == "error": + logger.error(f"Batch runner error: {result.get('message')}") + + # Wait for process to complete + await proc.wait() + + try: + await asyncio.wait_for(_stream_results(), timeout=aggregate_timeout) + except asyncio.TimeoutError: + logger.warning( + f"[{self._container_name}] Batch timed out after {aggregate_timeout:.1f}s " + f"({len(received_test_ids)}/{len(spec.tests)} tests received)" + ) + proc.kill() + await proc.wait() + + # Yield all collected results + for result_type, result in streamed_results: + yield result + + except asyncio.TimeoutError: + logger.warning(f"Batch execution timed out after {aggregate_timeout:.1f}s") + + except Exception as e: + logger.warning(f"Batch execution stream broke: {e}") + + # Handle missing tests (stream truncated before "done") + if not received_done and compile_result is None: + # No compile result received - emit a failure + compile_result = CompileResult( + status=CompileStatus.UNKNOWN_ERROR, + error_message="Batch execution terminated unexpectedly", + duration_ms=(time.perf_counter() - batch_start) * 1000, + ) + yield compile_result + + if not received_done and (compile_result is None or compile_result.success): + # Some tests may not have been run + for i, test in enumerate(spec.tests): + test_id = test.id or f"test_{i}" + if test_id not in received_test_ids: + run_ms = (time.perf_counter() - run_start) * 1000 + yield ExecutionResult( + compile_result=compile_result + or CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SANDBOX_ERROR, + stdout="", + stderr="Batch execution terminated unexpectedly", + exit_code=None, + run_duration_ms=run_ms, + total_duration_ms=run_ms, + ) + + def _build_batch_tar( + self, + manifest: dict, + code: str, + runner_script: str, + batch_dir: str = "_batch", + ) -> bytes: + """Build tar archive containing batch execution files. + + Creates a tar with: + - {batch_dir}/manifest.json: Test configuration + - {batch_dir}/solution.py: Code under test + - {batch_dir}/batch_runner.py: Self-contained test runner + + Args: + manifest: Test configuration dict + code: Python code to test + runner_script: Content of batch_runner.py + batch_dir: Directory name within workspace + + Returns: + Tar archive bytes + """ + buf = io.BytesIO() + with tarfile.open(fileobj=buf, mode="w") as tar: + # Create directory entry first + dir_info = tarfile.TarInfo(name=batch_dir) + dir_info.type = tarfile.DIRTYPE + dir_info.mode = 0o755 + dir_info.mtime = int(time.time()) + tar.addfile(dir_info) + + # Add manifest.json + manifest_data = json.dumps(manifest, indent=2).encode("utf-8") + info = tarfile.TarInfo(name=f"{batch_dir}/manifest.json") + info.size = len(manifest_data) + info.mtime = int(time.time()) + tar.addfile(info, io.BytesIO(manifest_data)) + + # Add solution.py + code_data = code.encode("utf-8") + info = tarfile.TarInfo(name=f"{batch_dir}/solution.py") + info.size = len(code_data) + info.mtime = int(time.time()) + tar.addfile(info, io.BytesIO(code_data)) + + # Add batch_runner.py + runner_data = runner_script.encode("utf-8") + info = tarfile.TarInfo(name=f"{batch_dir}/batch_runner.py") + info.size = len(runner_data) + info.mtime = int(time.time()) + tar.addfile(info, io.BytesIO(runner_data)) + + buf.seek(0) + return buf.read() + + async def _write_tar( + self, + tar_data: bytes, + *, + timeout_s: float = 5.0, + ) -> None: + """Write a tar archive directly to the container. + + Similar to _write_file but takes raw tar bytes. + """ + if self._workspace_host_dir: + # Extract tar directly to host filesystem - no podman exec + buf = io.BytesIO(tar_data) + with tarfile.open(fileobj=buf, mode="r") as tar: + tar.extractall(path=self._workspace_host_dir) + return + + await asyncio.wait_for( + self._run_podman( + "exec", + "-i", + self._container_name, + "tar", + "-xC", + self._config.working_dir, + check=True, + capture=True, + input_data=tar_data, + ), + timeout=timeout_s, + ) + + +@dataclass +class PodmanResult: + """Result of a podman-hpc command.""" + + returncode: int + stdout: str + stderr: str + + +class PodmanError(Exception): + """Error from podman-hpc command.""" + + pass + + +class PodmanHPCSandboxPool(BaseSandboxPool[PodmanHPCSandbox]): + """ + Pool of persistent Podman-HPC containers with LRU caching. + + Manages container lifecycle, checkout/release, and execution caching. + Designed for HPC environments with Slurm job scheduling. + + Inherits from BaseSandboxPool to use background reset pattern: + - checkout() returns pre-reset sandboxes instantly + - release() spawns background reset task + - shutdown() waits for pending resets before cleanup + """ + + def __init__( + self, + n_workers: int = 4, + image: str = "python:3.11-slim", + config: Optional[PodmanConfig] = None, + cache_size: int = 10000, + auto_replace_failed: bool = True, + max_consecutive_failures: int = 5, + max_concurrent_ops: int = 8, + workspace_base_dir: str = "auto", + ): + """ + Initialize Podman-HPC sandbox pool. + + Args: + n_workers: Number of sandboxes to create + image: Podman image (e.g., "python:3.11-slim") + config: Podman-specific configuration + cache_size: Maximum entries in execution cache + auto_replace_failed: If True, create new sandbox when reset fails + max_consecutive_failures: Maximum consecutive reset failures before raising + SandboxPoolExhaustedError (circuit breaker threshold) + max_concurrent_ops: Maximum concurrent operations (resets, executions) + workspace_base_dir: Base directory for bind mounts. Options: + - "auto" (default): Auto-detect; use /local if on HPC, else None + - explicit path: Use specified directory for bind mounts + - None: Disable bind mounts, use tar-based I/O + """ + super().__init__( + n_workers=n_workers, + cache_size=cache_size, + auto_replace_failed=auto_replace_failed, + max_consecutive_failures=max_consecutive_failures, + max_concurrent_ops=max_concurrent_ops, + ) + self._image = image + self._config = config or PodmanConfig() + self._exec_semaphore: Optional[asyncio.Semaphore] = None + + # Extract Python version from image name + self._python_version = self._parse_python_version(image) + + # Resolve workspace_base_dir + if workspace_base_dir == "auto": + # Auto-detect: use /local if on HPC, else None + slurm_job_id = os.environ.get("SLURM_JOB_ID") + if ( + slurm_job_id and Path("/home/u5ds/joanv.u5ds").exists() + ): # TODO [joan]: Remove hardcoding + self._workspace_base_dir: Optional[str] = ( + f"/home/u5ds/joanv.u5ds/sandbox/ludic-{slurm_job_id}" + ) + else: + self._workspace_base_dir = None + else: + self._workspace_base_dir = workspace_base_dir + + @property + def python_version(self) -> str: + """Python version used by sandboxes in this pool.""" + return self._python_version + + # ------------------------------------------------------------------------- + # Abstract method implementations (backend-specific logic) + # ------------------------------------------------------------------------- + + async def _create_sandboxes(self) -> List[PodmanHPCSandbox]: + """ + Create and start all Podman-HPC container sandboxes. + + Pulls the image (auto-migrates to shared storage on HPC) and creates + persistent containers in parallel. + + Returns: + List of started PodmanHPCSandbox instances + """ + # Create shared exec semaphore (prevents podman deadlock) + self._exec_semaphore = asyncio.Semaphore(self._max_concurrent_ops) + logger.info( + f"Podman exec semaphore initialized: max_concurrent_ops={self._max_concurrent_ops}" + ) + + # Pull image (podman-hpc pull auto-migrates to shared storage) + logger.info( + f"Pulling image {self._image} (may take a moment for HPC migration)..." + ) + proc = await asyncio.create_subprocess_exec( + "podman-hpc", + "pull", + self._image, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + await proc.communicate() + + # If using bind mounts, create base directory + if self._workspace_base_dir: + Path(self._workspace_base_dir).mkdir(parents=True, exist_ok=True) + logger.info(f"Bind mount enabled: {self._workspace_base_dir}") + else: + logger.info("Bind mount disabled, using tar-based I/O") + + # Create and start sandboxes in parallel + container_prefix = _get_container_name_prefix() + + async def _create_and_start(i: int) -> PodmanHPCSandbox: + container_name = f"{container_prefix}-{i}" + + # Create per-sandbox host directory if using bind mounts + workspace_host_dir = None + if self._workspace_base_dir: + workspace_host_dir = f"{self._workspace_base_dir}/sandbox-{i}" + Path(workspace_host_dir).mkdir(parents=True, exist_ok=True) + + sandbox = PodmanHPCSandbox( + container_name=container_name, + image=self._image, + config=self._config, + python_version=self._python_version, + exec_semaphore=self._exec_semaphore, # Shared across all sandboxes + workspace_host_dir=workspace_host_dir, + ) + await sandbox.start() + return sandbox + + sandboxes = await asyncio.gather( + *[_create_and_start(i) for i in range(self._n_workers)] + ) + + logger.info(f"Podman-HPC sandbox pool ready ({self._n_workers} workers)") + return sandboxes + + async def _stop_sandbox(self, sandbox: PodmanHPCSandbox) -> None: + """ + Stop and remove a single Podman container. + + Called during shutdown and when replacing a failed sandbox. + Handles errors gracefully (logs warnings, doesn't raise). + + Args: + sandbox: The sandbox to stop + """ + try: + await sandbox.stop() + except Exception as e: + logger.warning(f"Failed to stop Podman container: {e}") + + async def _create_replacement_sandbox(self) -> Optional[PodmanHPCSandbox]: + """ + Create a single replacement sandbox for a failed one. + + Creates a new container with the same configuration and starts it. + + Returns: + New PodmanHPCSandbox instance, or None if creation fails + """ + try: + container_prefix = _get_container_name_prefix() + # Use timestamp to ensure unique container name + container_name = f"{container_prefix}-replacement-{int(time.time())}" + + # Create per-sandbox host directory if using bind mounts + workspace_host_dir = None + if self._workspace_base_dir: + workspace_host_dir = ( + f"{self._workspace_base_dir}/sandbox-replacement-{int(time.time())}" + ) + Path(workspace_host_dir).mkdir(parents=True, exist_ok=True) + + sandbox = PodmanHPCSandbox( + container_name=container_name, + image=self._image, + config=self._config, + python_version=self._python_version, + exec_semaphore=self._exec_semaphore, # Use shared semaphore + workspace_host_dir=workspace_host_dir, + ) + await sandbox.start() + logger.info(f"Created replacement Podman sandbox: {container_name}") + return sandbox + except Exception as e: + logger.error(f"Failed to create replacement Podman sandbox: {e}") + return None + + async def shutdown(self) -> None: + """ + Shutdown pool and clean up resources. + + Stops all sandboxes and removes workspace directories if using bind mounts. + """ + # Call parent shutdown to stop sandboxes + await super().shutdown() + + # Clean up host workspace directories + if self._workspace_base_dir: + workspace_path = Path(self._workspace_base_dir) + if workspace_path.exists(): + try: + shutil.rmtree(self._workspace_base_dir, ignore_errors=True) + logger.info( + f"Cleaned up workspace directory: {self._workspace_base_dir}" + ) + except Exception as e: + logger.warning(f"Failed to clean up workspace directory: {e}") + + # ------------------------------------------------------------------------- + # Helper methods + # ------------------------------------------------------------------------- + + @staticmethod + def _parse_python_version(image: str) -> str: + """Extract Python version from image name.""" + # Common patterns: python:3.11-slim, python:3.11, ghcr.io/.../python:3.11 + match = re.search(r"python:(\d+\.\d+)", image) + if match: + return match.group(1) + return "3.11" # Default fallback diff --git a/src/ludic/envs/code_exec/pool.py b/src/ludic/envs/code_exec/pool.py new file mode 100644 index 0000000..8a8cde4 --- /dev/null +++ b/src/ludic/envs/code_exec/pool.py @@ -0,0 +1,483 @@ +""" +Base sandbox pool with background reset pattern. + +Provides shared pool management logic for Docker, Podman, and other backends. +The background reset pattern ensures that sandbox cleanup happens off the +critical path, maximizing throughput for rollout generation. +""" + +from __future__ import annotations + +import asyncio +import logging +from abc import ABC, abstractmethod +from typing import Dict, Generic, List, Optional, Set, TypeVar + +from .cache import LRUCache +from .sandbox import Sandbox +from .types import BatchTestResult, SandboxPoolExhaustedError + +logger = logging.getLogger(__name__) + +# Type variable for sandbox implementations +S = TypeVar("S", bound=Sandbox) + + +class BaseSandboxPool(ABC, Generic[S]): + """ + Abstract base class for sandbox pools with background reset. + + Provides queue-based checkout/release with sandboxes reset in background + tasks (off critical path). Includes LRU caching, pending task tracking, + and error handling for failed resets. + + Subclasses must implement: _create_sandboxes(), _stop_sandbox(), python_version. + + Background Reset Pattern: + Released sandboxes are reset asynchronously and returned to the queue. + checkout() receives already-clean sandboxes instantly, hiding reset latency. + Failed resets discard the sandbox and optionally create a replacement. + """ + + def __init__( + self, + n_workers: int = 4, + cache_size: int = 10000, + auto_replace_failed: bool = True, + max_consecutive_failures: int = 5, + max_concurrent_ops: int = 8, + ): + """ + Initialize the pool. + + Args: + n_workers: Number of sandboxes to create + cache_size: Maximum entries in the execution cache + auto_replace_failed: If True, create new sandbox when reset fails + max_consecutive_failures: Maximum consecutive reset failures before raising + SandboxPoolExhaustedError (circuit breaker threshold) + max_concurrent_ops: Maximum concurrent sandbox operations (resets, exec + calls). Prevents podman/docker deadlock with too many simultaneous calls. + """ + self._n_workers = n_workers + self._cache = LRUCache(max_size=cache_size) + self._auto_replace_failed = auto_replace_failed + self._max_consecutive_failures = max_consecutive_failures + self._max_concurrent_ops = max_concurrent_ops + + self._sandboxes: List[S] = [] + self._queue: Optional[asyncio.Queue[S]] = None + self._pending_resets: Set[asyncio.Task] = set() + self._started = False + self._shutting_down = False + self._consecutive_failures = 0 + + # ------------------------------------------------------------------------- + # Abstract methods (must be implemented by subclasses) + # ------------------------------------------------------------------------- + + @property + @abstractmethod + def python_version(self) -> str: + """Python version used by sandboxes in this pool.""" + ... + + @abstractmethod + async def _create_sandboxes(self) -> List[S]: + """ + Create all sandbox instances. + + Called by start(). Should create n_workers sandboxes, start them, + and return the list. This is where backend-specific logic lives + (Docker container creation, Podman-HPC setup, etc.). + + Returns: + List of started sandbox instances + """ + ... + + @abstractmethod + async def _stop_sandbox(self, sandbox: S) -> None: + """ + Stop and cleanup a single sandbox. + + Called during shutdown and when replacing a failed sandbox. + Should handle errors gracefully (log warnings, don't raise). + + Args: + sandbox: The sandbox to stop + """ + ... + + async def _create_replacement_sandbox(self) -> Optional[S]: + """ + Create a single replacement sandbox. + + Called when a sandbox fails to reset and auto_replace_failed is True. + Default implementation returns None (no replacement). Override in + subclass if dynamic sandbox creation is supported. + + Returns: + New sandbox instance, or None if replacement not supported + """ + return None + + # ------------------------------------------------------------------------- + # Pool lifecycle + # ------------------------------------------------------------------------- + + @property + def available(self) -> int: + """Number of sandboxes currently available for checkout.""" + if self._queue is None: + return 0 + return self._queue.qsize() + + @property + def cache_stats(self) -> Dict[str, int]: + """Cache statistics (hits, misses, size, max_size).""" + return self._cache.stats + + @property + def pending_resets(self) -> int: + """Number of background reset tasks currently running.""" + return len(self._pending_resets) + + async def start(self) -> None: + """ + Initialize the pool. + + Creates all sandboxes and makes them available for checkout. + Idempotent - calling multiple times has no effect. + """ + if self._started: + return + + # Limits concurrent background reset TASKS (admission control) + # Prevents podman/docker deadlock with too many simultaneous operations + self._ops_semaphore = asyncio.Semaphore(self._max_concurrent_ops) + logger.info( + f"Pool starting: n_workers={self._n_workers}, " + f"max_concurrent_ops={self._max_concurrent_ops}" + ) + + # Create sandboxes (backend-specific) + self._sandboxes = await self._create_sandboxes() + + # Create queue and populate with all sandboxes + self._queue = asyncio.Queue() + for sandbox in self._sandboxes: + await self._queue.put(sandbox) + + self._started = True + logger.info( + f"Pool started: {len(self._sandboxes)} sandboxes ready, " + f"queue_size={self._queue.qsize()}" + ) + + async def shutdown(self) -> None: + """ + Tear down all sandboxes and release resources. + + Waits for all pending reset tasks to complete before stopping + sandboxes, ensuring clean shutdown without orphaned tasks. + """ + if not self._started: + return + + self._shutting_down = True + + # Wait for all pending reset tasks to complete + if self._pending_resets: + logger.debug(f"Waiting for {len(self._pending_resets)} pending resets...") + await asyncio.gather(*self._pending_resets, return_exceptions=True) + + # Stop all sandboxes + for sandbox in self._sandboxes: + await self._stop_sandbox(sandbox) + + self._sandboxes.clear() + self._started = False + self._queue = None + self._shutting_down = False + + async def drain_pending_resets(self, timeout_s: float = 60.0) -> int: + """ + Wait for all pending reset tasks to complete. + + Call this before switching between high-concurrency phases + (e.g., before eval after training step) to ensure all sandboxes + are available in the queue. + + Uses asyncio.wait() instead of wait_for(gather()) to avoid + cancelling tasks on timeout (which would destroy sandboxes). + + Args: + timeout_s: Maximum time to wait for resets to complete + + Returns: + Number of resets that completed + """ + if not self._pending_resets: + logger.debug("Drain called but no pending resets") + return 0 + + # Snapshot the current tasks (set may change during await) + tasks = list(self._pending_resets) + count = len(tasks) + logger.info( + f"Draining {count} pending resets... " + f"queue: {self.available}/{self._n_workers}" + ) + + import time + start = time.time() + + # Use wait() instead of wait_for(gather()) - doesn't cancel on timeout + done, pending = await asyncio.wait(tasks, timeout=timeout_s) + + elapsed = time.time() - start + + if pending: + logger.warning( + f"Drain timeout after {elapsed:.1f}s! " + f"Completed: {len(done)}, still pending: {len(pending)}, " + f"queue: {self.available}/{self._n_workers}" + ) + else: + logger.info( + f"Drain complete in {elapsed:.1f}s: " + f"{len(done)} resets finished, " + f"queue: {self.available}/{self._n_workers}" + ) + + return len(done) + + # ------------------------------------------------------------------------- + # Checkout / Release with background reset + # ------------------------------------------------------------------------- + + async def checkout(self, timeout_s: float = 30.0) -> Sandbox: + """ + Get exclusive access to a sandbox. + + The returned sandbox is guaranteed to be in a clean state (reset + was performed in the background after the previous release). + + Waits on the queue for a sandbox to become available. Background + resets are rate-limited by semaphore to prevent backend deadlock. + + Args: + timeout_s: Maximum time to wait for a sandbox + + Returns: + Exclusive Sandbox handle + + Raises: + RuntimeError: If pool not started + TimeoutError: If no sandbox available within timeout + """ + if not self._started or self._queue is None: + raise RuntimeError("Pool not started. Call start() first.") + + import time + + start_time = time.monotonic() + deadline = start_time + timeout_s + attempt = 0 + + while True: + remaining = deadline - time.monotonic() + attempt += 1 + + if remaining <= 0: + # Detailed timeout diagnostics + semaphore_free = self._ops_semaphore._value if self._ops_semaphore else 0 + logger.error( + f"CHECKOUT TIMEOUT after {timeout_s}s! " + f"Pool: {self._n_workers}, available: {self.available}, " + f"pending_resets: {self.pending_resets}, " + f"semaphore: {semaphore_free}/{self._max_concurrent_ops} free, " + f"attempts: {attempt}" + ) + raise TimeoutError( + f"No sandbox available after {timeout_s}s. " + f"Pool size: {self._n_workers}, available: {self.available}, " + f"pending resets: {self.pending_resets}" + ) + + # Log if we're waiting with empty queue + if self._queue.empty() and attempt == 1: + logger.info( + f"Checkout waiting: queue empty, " + f"pending_resets: {self.pending_resets}" + ) + + try: + sandbox = await asyncio.wait_for( + self._queue.get(), + timeout=min(remaining, 5.0), # Short timeout to recheck + ) + wait_time = time.monotonic() - start_time + if wait_time > 1.0: + logger.info( + f"Checkout OK after {wait_time:.2f}s wait, " + f"queue now: {self._queue.qsize()}/{self._n_workers}" + ) + return sandbox + except asyncio.TimeoutError: + # Log periodic status during long waits + elapsed = time.monotonic() - start_time + logger.warning( + f"Checkout still waiting after {elapsed:.1f}s: " + f"available: {self.available}, pending: {self.pending_resets}" + ) + continue + + async def release(self, sandbox: Sandbox) -> None: + """ + Return a sandbox to the pool. + + The sandbox is reset in a background task, then returned to the + available queue. This makes release() return immediately without + blocking the caller. + + Args: + sandbox: The sandbox to release (must have been obtained via checkout) + + Raises: + RuntimeError: If pool not started + """ + if not self._started or self._queue is None: + raise RuntimeError("Pool not started") + + if self._shutting_down: + # During shutdown, don't spawn new tasks + return + + # Spawn background reset task + task = asyncio.create_task( + self._background_reset(sandbox), # type: ignore + name=f"sandbox-reset-{id(sandbox)}", + ) + self._pending_resets.add(task) + task.add_done_callback(self._pending_resets.discard) + + async def _background_reset(self, sandbox: S) -> None: + """ + Reset sandbox and return to queue (runs in background). + + Uses semaphore to limit concurrent reset operations (prevents + podman deadlock with too many simultaneous exec calls). + + On success, the sandbox is returned to the available queue. + On failure, the sandbox is discarded and optionally replaced. + """ + import time + + sandbox_id = id(sandbox) % 10000 # Short ID for logging + wait_start = time.time() + + # Limit concurrent ops to prevent podman/docker deadlock + async with self._ops_semaphore: + wait_elapsed = time.time() - wait_start + if wait_elapsed > 0.1: + logger.debug(f"[SB-{sandbox_id}] Semaphore acquired after {wait_elapsed:.2f}s wait") + + reset_start = time.time() + try: + await sandbox.reset() + reset_elapsed = time.time() - reset_start + total_elapsed = time.time() - wait_start + + if self._queue is not None and not self._shutting_down: + await self._queue.put(sandbox) + logger.debug( + f"[SB-{sandbox_id}] Reset OK: {reset_elapsed:.2f}s reset, " + f"{total_elapsed:.2f}s total. " + f"Queue now: {self._queue.qsize()}/{self._n_workers}" + ) + except Exception as e: + reset_elapsed = time.time() - reset_start + logger.error( + f"[SB-{sandbox_id}] Reset FAILED after {reset_elapsed:.2f}s: {e}" + ) + await self._handle_reset_failure(sandbox, e) + + async def _handle_reset_failure(self, sandbox: S, error: Exception) -> None: + """ + Handle a sandbox that failed to reset. + + Logs the error, removes the sandbox from the pool, and optionally + creates a replacement. Implements circuit breaker pattern to detect + systemic failures. + + Raises: + SandboxPoolExhaustedError: If consecutive failures exceed threshold + """ + # Increment failure counter + self._consecutive_failures += 1 + + logger.warning( + f"Sandbox reset failed: {error}. Discarding sandbox. " + f"Consecutive failures: {self._consecutive_failures}/{self._max_consecutive_failures}" + ) + + # Check circuit breaker threshold + if self._consecutive_failures >= self._max_consecutive_failures: + logger.error( + f"Circuit breaker triggered: {self._consecutive_failures} consecutive " + f"sandbox reset failures. Pool is exhausted." + ) + raise SandboxPoolExhaustedError( + f"Sandbox pool exhausted after {self._consecutive_failures} consecutive " + f"reset failures. This indicates a systemic issue requiring operator intervention." + ) + + # Remove from tracked sandboxes + if sandbox in self._sandboxes: + self._sandboxes.remove(sandbox) + + # Try to stop the failed sandbox + try: + await self._stop_sandbox(sandbox) + except Exception as stop_error: + logger.warning(f"Failed to stop broken sandbox: {stop_error}") + + # Optionally create replacement + if self._auto_replace_failed and not self._shutting_down: + try: + replacement = await self._create_replacement_sandbox() + if replacement is not None: + self._sandboxes.append(replacement) + if self._queue is not None: + await self._queue.put(replacement) + # Reset failure counter on successful replacement + self._consecutive_failures = 0 + logger.info( + "Created replacement sandbox after reset failure. " + "Consecutive failure counter reset." + ) + except Exception as create_error: + logger.warning(f"Failed to create replacement sandbox: {create_error}") + + # ------------------------------------------------------------------------- + # Cache interface + # ------------------------------------------------------------------------- + + def get_cached( + self, + code_hash: str, + tests_hash: str, + ) -> Optional[BatchTestResult]: + """Check cache for result (sync, thread-safe).""" + return self._cache.get(code_hash, tests_hash) + + def put_cached( + self, + code_hash: str, + tests_hash: str, + result: BatchTestResult, + ) -> None: + """Store result in cache (sync, thread-safe).""" + self._cache.put(code_hash, tests_hash, result) diff --git a/src/ludic/envs/code_exec/runners.py b/src/ludic/envs/code_exec/runners.py new file mode 100644 index 0000000..6f0f93d --- /dev/null +++ b/src/ludic/envs/code_exec/runners.py @@ -0,0 +1,620 @@ +""" +Code runners for executing code against test cases. + +This module defines the CodeRunner protocol and concrete implementations +for different test execution strategies (stdin/stdout, function calls, etc.). + +The runner is responsible for: + 1. Orchestrating compilation and execution via a Sandbox + 2. Running code against multiple TestCases + 3. Using an OutputVerifier to compare results + 4. Building rich TestResult and BatchTestResult objects +""" + +from __future__ import annotations + +import asyncio +import hashlib +import json +import logging +from typing import List, Optional, Protocol, Set, runtime_checkable + +from .adapters.base import OutputVerifier +from .sandbox import Sandbox +from .types import ( + BatchExecutionSpec, + BatchTestResult, + CompileResult, + CompileStatus, + ExecutionResult, + RunStatus, + TestCase, + TestResult, +) + +logger = logging.getLogger(__name__) + + +def compute_hash(content: str) -> str: + """ + Compute SHA256 hash, return first 16 hex chars. + + This is used for cache keys to uniquely identify code and test sets. + 16 hex chars = 64 bits, which gives collision probability < 1e-10 + for reasonable dataset sizes. + + Args: + content: String to hash + + Returns: + First 16 characters of SHA256 hex digest + """ + return hashlib.sha256(content.encode()).hexdigest()[:16] + + +def hash_tests(tests: List[TestCase]) -> str: + """ + Compute stable hash of test cases for caching. + + Creates a deterministic hash by converting test inputs and expected + outputs to a canonical JSON representation with sorted keys, then hashing. + + Args: + tests: List of test cases to hash + + Returns: + 16-character hash string + """ + # Use JSON with sorted keys for deterministic serialization + content = json.dumps( + [(t.input, t.expected) for t in tests], + sort_keys=True, + default=str, # Handle non-JSON-serializable types + ) + return compute_hash(content) + + +@runtime_checkable +class CodeRunner(Protocol): + """ + Protocol for running code against test cases. + + A runner orchestrates the interaction between a Sandbox and test cases, + using an OutputVerifier to determine if each test passes. It handles + compilation, execution, error recovery, and early stopping. + + Implementations should be stateless and reusable across multiple + test runs. All state is passed explicitly via arguments. + """ + + async def run_tests( + self, + sandbox: Sandbox, + code: str, + tests: List[TestCase], + *, + verifier: OutputVerifier, + stop_on_first_failure: bool = False, + compile_first: bool = True, + ) -> BatchTestResult: + """ + Run code against all test cases and return aggregated results. + + Args: + sandbox: Sandbox to execute code in (must be checked out) + code: Source code to test + tests: List of test cases to run + verifier: Verifier to compare actual vs expected output + stop_on_first_failure: If True, skip remaining tests after first failure + compile_first: If True, compile once before running tests + + Returns: + BatchTestResult with individual test results and metadata + """ + ... + + +class StdinStdoutRunner: + """ + Runner for APPS-style stdin/stdout testing. + + This runner executes code that reads from stdin and writes to stdout, + comparing the output against expected values. This is the standard + format for competitive programming problems (Codeforces, APPS, etc.). + + Each test case's `input` field is passed as stdin, and the `expected` + field is compared against stdout using the provided verifier. + + Design notes: + - Default timeout is 5.0s for efficiency (per user specification) + - Compilation is checked first by default to get early failure signal + - All operations are async to avoid blocking the event loop + - Rich error details in TestResult.comparison_details + """ + + def __init__( + self, + default_timeout_s: float = 5.0, + memory_limit_mb: Optional[int] = 256, + use_batch_execution: bool = True, + ) -> None: + """ + Initialize the runner with default resource limits. + + Args: + default_timeout_s: Default execution timeout per test (seconds). + Tests can override via metadata["timeout_s"]. + memory_limit_mb: Memory limit for execution (None = no limit) + use_batch_execution: If True and sandbox supports it, use batched + execution to reduce semaphore acquisitions. + """ + self._default_timeout_s = default_timeout_s + self._memory_limit_mb = memory_limit_mb + self._use_batch_execution = use_batch_execution + + async def run_tests( + self, + sandbox: Sandbox, + code: str, + tests: List[TestCase], + *, + verifier: OutputVerifier, + stop_on_first_failure: bool = False, + compile_first: bool = True, + ) -> BatchTestResult: + """ + Run stdin/stdout tests against code. + + Implementation steps: + 1. Compute code_hash and tests_hash for caching + 2. If compile_first=True, compile code and fail fast if it fails + 3. For each test: + - Execute with test.input as stdin + - Compare stdout against test.expected using verifier + - Build TestResult with full metadata + 4. If stop_on_first_failure=True, mark remaining tests NOT_RUN + 5. Return BatchTestResult + + Args: + sandbox: Sandbox to execute code in (must be checked out) + code: Source code to test + tests: List of test cases (input/expected are stdin/stdout strings) + verifier: Verifier to compare stdout vs expected + stop_on_first_failure: If True, skip remaining tests after first failure + compile_first: If True, compile once before running tests + + Returns: + BatchTestResult with results for each test + """ + import time + + run_start = time.perf_counter() + + # Compute hashes for caching + code_hash = compute_hash(code) + tests_hash_val = hash_tests(tests) + + # Use batch execution if enabled and sandbox supports it + has_batch = hasattr(sandbox, "execute_batch") + logger.debug( + f"run_tests: use_batch={self._use_batch_execution}, " + f"has_execute_batch={has_batch}, num_tests={len(tests)}" + ) + + if self._use_batch_execution and has_batch: + result = await self._run_tests_batched( + sandbox=sandbox, + code=code, + tests=tests, + verifier=verifier, + stop_on_first_failure=stop_on_first_failure, + compile_first=compile_first, + code_hash=code_hash, + tests_hash=tests_hash_val, + ) + elapsed_ms = (time.perf_counter() - run_start) * 1000 + logger.debug( + f"Batch execution completed: {len(tests)} tests in {elapsed_ms:.1f}ms, " + f"passed={result.passed_count}/{result.total_count}" + ) + return result + + # Non-batch execution + # Step 1: Compile first if requested + compile_result: Optional[CompileResult] = None + if compile_first: + compile_result = await sandbox.compile( + code, + timeout_s=self._default_timeout_s, + ) + + # If compilation failed, all tests fail without execution + if not compile_result.success: + return self._create_all_failed_batch( + tests=tests, + code_hash=code_hash, + tests_hash=tests_hash_val, + compile_result=compile_result, + reason="compilation_failed", + ) + + # Step 2: Run tests (in parallel when possible) + if stop_on_first_failure: + # Sequential execution with early stopping + results: List[TestResult] = [] + for test_case in tests: + # Get timeout for this test (allow per-test override) + timeout_s = test_case.metadata.get("timeout_s", self._default_timeout_s) + memory_limit = test_case.metadata.get( + "memory_limit_mb", self._memory_limit_mb + ) + + # Execute the test + test_result = await self._run_single_test( + sandbox=sandbox, + code=code, + test_case=test_case, + verifier=verifier, + timeout_s=timeout_s, + memory_limit_mb=memory_limit, + skip_compile=compile_first, # Skip if we already compiled + ) + + results.append(test_result) + + # Stop on first failure + if not test_result.passed: + # Mark remaining tests as NOT_RUN + for remaining_test in tests[len(results) :]: + not_run_result = self._create_not_run_result( + test_case=remaining_test, + code_hash=code_hash, + ) + results.append(not_run_result) + break + else: + # Parallel execution with asyncio.gather + async def run_test_with_metadata(test_case: TestCase) -> TestResult: + timeout_s = test_case.metadata.get("timeout_s", self._default_timeout_s) + memory_limit = test_case.metadata.get( + "memory_limit_mb", self._memory_limit_mb + ) + return await self._run_single_test( + sandbox=sandbox, + code=code, + test_case=test_case, + verifier=verifier, + timeout_s=timeout_s, + memory_limit_mb=memory_limit, + skip_compile=compile_first, # Skip if we already compiled + ) + + # Run all tests in parallel + results = await asyncio.gather( + *[run_test_with_metadata(test) for test in tests] + ) + + return BatchTestResult( + results=list(results), + code_hash=code_hash, + tests_hash=tests_hash_val, + ) + + async def _run_single_test( + self, + sandbox: Sandbox, + code: str, + test_case: TestCase, + verifier: OutputVerifier, + timeout_s: float, + memory_limit_mb: Optional[int], + skip_compile: bool = False, + ) -> TestResult: + """ + Run a single test case. + + Args: + sandbox: Sandbox to execute in + code: Source code + test_case: Test to run + verifier: Output verifier + timeout_s: Execution timeout + memory_limit_mb: Memory limit + skip_compile: If True, skip compilation (assumes already compiled) + + Returns: + TestResult for this test + """ + # Execute code with test input + execution = await sandbox.execute( + code=code, + stdin=str(test_case.input), # Ensure input is string + skip_compile=skip_compile, + timeout_s=timeout_s, + memory_limit_mb=memory_limit_mb, + ) + + # If execution failed (didn't compile or runtime error), test fails + if not execution.succeeded: + return TestResult( + test_case=test_case, + passed=False, + actual=execution.stdout, + execution=execution, + comparison_details=self._get_execution_failure_details(execution), + ) + + # Execution succeeded, compare output + actual_output = execution.stdout + expected_output = str(test_case.expected) + + passed, comparison_details = verifier.verify(actual_output, expected_output) + + return TestResult( + test_case=test_case, + passed=passed, + actual=actual_output, + execution=execution, + comparison_details=comparison_details, + ) + + async def _run_tests_batched( + self, + sandbox: Sandbox, + code: str, + tests: List[TestCase], + verifier: OutputVerifier, + stop_on_first_failure: bool, + compile_first: bool, + code_hash: str, + tests_hash: str, + ) -> BatchTestResult: + """ + Run tests using batch execution API with crash resilience. + + This method uses the sandbox's execute_batch() to run all tests + in a single podman exec call, reducing semaphore acquisitions + from O(2N) to O(2). + + Args: + sandbox: Sandbox with execute_batch() method + code: Source code to test + tests: List of test cases + verifier: Output verifier for comparing results + stop_on_first_failure: If True, stop after first failure + compile_first: If True, compile before running tests + code_hash: Pre-computed hash of code + tests_hash: Pre-computed hash of tests + + Returns: + BatchTestResult with results for each test + """ + spec = BatchExecutionSpec( + code=code, + tests=tests, + compile_first=compile_first, + timeout_s=self._default_timeout_s, + stop_on_first_failure=stop_on_first_failure, + ) + + results: List[TestResult] = [] + compile_result: Optional[CompileResult] = None + received_done = False + received_test_ids: Set[str] = set() + + # Build lookup for test cases by ID + test_by_id = {t.id: t for t in tests} + + try: + async for result in sandbox.execute_batch(spec): + if isinstance(result, CompileResult): + compile_result = result + if not result.success: + # Compilation failed - return batch with all tests failed + return self._create_all_failed_batch( + tests=tests, + code_hash=code_hash, + tests_hash=tests_hash, + compile_result=compile_result, + reason="compilation_failed", + ) + elif isinstance(result, ExecutionResult): + # This is a test result - find the matching test case + # The execute_batch implementation tags results with test_id + # in the cache_key field + test_id = result.cache_key or "" + received_test_ids.add(test_id) + + test_case = test_by_id.get(test_id) + if test_case is None: + logger.warning( + f"Received result for unknown test_id: {test_id}" + ) + continue + + # Build TestResult from ExecutionResult + if not result.succeeded: + # Execution failed + test_result = TestResult( + test_case=test_case, + passed=False, + actual=result.stdout, + execution=result, + comparison_details=self._get_execution_failure_details( + result + ), + ) + else: + # Execution succeeded, compare output + actual_output = result.stdout + expected_output = str(test_case.expected) + passed, comparison_details = verifier.verify( + actual_output, expected_output + ) + test_result = TestResult( + test_case=test_case, + passed=passed, + actual=actual_output, + execution=result, + comparison_details=comparison_details, + ) + results.append(test_result) + elif isinstance(result, dict) and result.get("type") == "done": + received_done = True + break + + except Exception as e: + # Stream broke unexpectedly (OOM, container killed, etc.) + logger.warning(f"Batch execution stream broke: {e}") + + # Handle missing tests (stream truncated before "done") + if not received_done: + for test in tests: + if test.id not in received_test_ids: + # Create SANDBOX_ERROR result for missing tests + execution = ExecutionResult( + compile_result=compile_result + or CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SANDBOX_ERROR, + stdout="", + stderr="Batch execution terminated unexpectedly", + exit_code=None, + ) + results.append( + TestResult( + test_case=test, + passed=False, + actual="", + execution=execution, + comparison_details="Sandbox crashed before this test completed", + ) + ) + + return BatchTestResult( + results=results, + code_hash=code_hash, + tests_hash=tests_hash, + ) + + def _get_execution_failure_details(self, execution: ExecutionResult) -> str: + """ + Generate human-readable details for execution failures. + + Args: + execution: The failed execution result + + Returns: + Explanation of why execution failed + """ + # Compilation failure + if not execution.compiled: + compile_msg = execution.compile_result.error_message or "Unknown error" + if execution.compile_result.error_line is not None: + return f"Compilation failed at line {execution.compile_result.error_line}: {compile_msg}" + return f"Compilation failed: {compile_msg}" + + # Runtime failure + if execution.run_status == RunStatus.TIMEOUT: + return f"Execution timed out after {execution.run_duration_ms:.0f}ms" + + if execution.run_status == RunStatus.MEMORY_EXCEEDED: + return "Memory limit exceeded" + + if execution.run_status == RunStatus.RUNTIME_ERROR: + stderr = execution.stderr.strip() + if stderr: + # Show first few lines of stderr for debugging + stderr_lines = stderr.split("\n") + preview = "\n".join(stderr_lines[:5]) + if len(stderr_lines) > 5: + preview += f"\n... ({len(stderr_lines) - 5} more lines)" + return f"Runtime error:\n{preview}" + return f"Runtime error (exit code {execution.exit_code})" + + # Other failure + return f"Execution failed with status: {execution.run_status}" + + def _create_all_failed_batch( + self, + tests: List[TestCase], + code_hash: str, + tests_hash: str, + compile_result: CompileResult, + reason: str, + ) -> BatchTestResult: + """ + Create a BatchTestResult where all tests failed due to compilation error. + + Args: + tests: All test cases + code_hash: Hash of the code + tests_hash: Hash of the tests + compile_result: The failed compilation result + reason: Reason for batch failure + + Returns: + BatchTestResult with all tests marked as failed + """ + results: List[TestResult] = [] + + for test_case in tests: + # Create ExecutionResult with the compile failure + execution = ExecutionResult( + compile_result=compile_result, + run_status=None, # Never ran + stdout="", + stderr="", + exit_code=None, + compile_duration_ms=compile_result.duration_ms, + run_duration_ms=0.0, + total_duration_ms=compile_result.duration_ms, + ) + + test_result = TestResult( + test_case=test_case, + passed=False, + actual="", + execution=execution, + comparison_details=self._get_execution_failure_details(execution), + ) + results.append(test_result) + + return BatchTestResult( + results=results, + code_hash=code_hash, + tests_hash=tests_hash, + ) + + def _create_not_run_result( + self, + test_case: TestCase, + code_hash: str, + ) -> TestResult: + """ + Create a TestResult for a test that was skipped. + + Args: + test_case: The test case that was skipped + code_hash: Hash of the code (for metadata) + + Returns: + TestResult marked as NOT_RUN + """ + # Create a minimal ExecutionResult indicating the test wasn't run + execution = ExecutionResult( + compile_result=CompileResult( + status=CompileStatus.SUCCESS # Compilation already succeeded + ), + run_status=RunStatus.NOT_RUN, + stdout="", + stderr="", + exit_code=None, + ) + + return TestResult( + test_case=test_case, + passed=False, + actual="", + execution=execution, + comparison_details="Test skipped (stop_on_first_failure=True)", + ) diff --git a/src/ludic/envs/code_exec/sandbox.py b/src/ludic/envs/code_exec/sandbox.py new file mode 100644 index 0000000..d2b2d81 --- /dev/null +++ b/src/ludic/envs/code_exec/sandbox.py @@ -0,0 +1,239 @@ +""" +Sandbox protocols for isolated code execution. + +These protocols define the contract for sandbox implementations. +The actual implementations (Docker, subprocess, etc.) live in separate modules. +""" + +from __future__ import annotations + +from typing import Dict, Optional, Protocol, runtime_checkable + +from .types import BatchTestResult, CompileResult, ExecutionResult + + +@runtime_checkable +class Sandbox(Protocol): + """ + Async handle to a single isolated execution environment. + + Invariants: + - A sandbox is exclusive to one env instance at a time + - reset() clears all state from previous executions + - All operations are async to avoid blocking the event loop + + Lifecycle: + 1. Obtained via SandboxPool.checkout() + 2. reset() called to ensure clean state + 3. compile() and/or execute() called as needed + 4. Returned via SandboxPool.release() + + Implementations should ensure: + - Network isolation (no external access) + - Resource limits (CPU, memory) + - Timeout enforcement + - Filesystem isolation between uses + """ + + @property + def python_version(self) -> str: + """Python version in this sandbox (e.g., '3.11').""" + ... + + async def reset(self) -> None: + """ + Clear filesystem, kill processes, restore to clean state. + + Must be called before first use and is automatically called + by SandboxPool.release(). + """ + ... + + async def compile( + self, + code: str, + *, + timeout_s: float = 5.0, + ) -> CompileResult: + """ + Syntax-check / compile code without executing. + + For Python: runs py_compile or ast.parse to catch syntax errors. + For compiled languages: runs the compiler. + + Args: + code: Source code to compile/check + timeout_s: Maximum time for compilation + + Returns: + CompileResult with status and error details if failed + """ + ... + + async def execute( + self, + code: str, + *, + stdin: str = "", + skip_compile: bool = False, + timeout_s: float = 10.0, + memory_limit_mb: Optional[int] = None, + env_vars: Optional[Dict[str, str]] = None, + ) -> ExecutionResult: + """ + Execute code and return rich results. + + Implicitly compiles first if not already compiled (unless skip_compile=True). + The compile result is included in the returned ExecutionResult. + + Args: + code: Source code to execute + stdin: Input to feed to the process via stdin + skip_compile: If True, skip compilation step (assumes code already compiled) + timeout_s: Maximum execution time (excluding compilation) + memory_limit_mb: Memory limit override (None uses sandbox default) + env_vars: Additional environment variables + + Returns: + ExecutionResult with compile status, output, timing, etc. + """ + ... + + +@runtime_checkable +class SandboxPool(Protocol): + """ + Async pool of reusable sandboxes with caching. + + The pool manages: + 1. Sandbox lifecycle (start/stop containers, processes, etc.) + 2. Checkout/release of exclusive sandbox handles + 3. Execution cache (code+tests -> result) + + Lifecycle: + 1. start() - Initialize pool (start containers, etc.) + 2. checkout() - Get exclusive sandbox access + 3. release() - Return sandbox to pool + 4. shutdown() - Tear down all sandboxes + + The pool should be started once at application startup and shared + across all CodeExecEnv instances via factory closure injection. + + Caching: + The pool maintains an LRU cache keyed by (code_hash, tests_hash). + This avoids redundant execution when the same code is submitted + for the same tests (common in GRPO where multiple generations + are evaluated against the same problem). + """ + + @property + def python_version(self) -> str: + """Python version used by sandboxes in this pool.""" + ... + + @property + def available(self) -> int: + """Number of sandboxes currently available for checkout.""" + ... + + @property + def cache_stats(self) -> Dict[str, int]: + """ + Cache statistics. + + Returns dict with keys: + - hits: number of cache hits + - misses: number of cache misses + - size: current cache size + - max_size: maximum cache size + """ + ... + + async def start(self) -> None: + """ + Initialize the pool. + + This starts all sandboxes (containers, processes, etc.). + Should be called once before any checkout() calls. + Idempotent - calling multiple times has no effect. + """ + ... + + async def checkout(self, timeout_s: float = 30.0) -> Sandbox: + """ + Get exclusive access to a sandbox. + + Blocks until a sandbox is available or timeout is reached. + The returned sandbox is guaranteed to be in a clean state. + + Args: + timeout_s: Maximum time to wait for a sandbox + + Returns: + Exclusive Sandbox handle + + Raises: + TimeoutError: If no sandbox available within timeout + """ + ... + + async def release(self, sandbox: Sandbox) -> None: + """ + Return a sandbox to the pool. + + The sandbox is automatically reset before being made available + to other callers. + + Args: + sandbox: The sandbox to release (must have been obtained via checkout) + """ + ... + + async def shutdown(self) -> None: + """ + Tear down all sandboxes and release resources. + + After shutdown(), the pool cannot be used again without calling start(). + """ + ... + + # ----- Cache interface ----- + + def get_cached( + self, + code_hash: str, + tests_hash: str, + ) -> Optional[BatchTestResult]: + """ + Check if we have a cached result for this code+tests pair. + + This is a synchronous method for use from env_step(). + Thread-safe. + + Args: + code_hash: Hash of the submitted code + tests_hash: Hash of the test cases + + Returns: + Cached BatchTestResult if found, None otherwise + """ + ... + + def put_cached( + self, + code_hash: str, + tests_hash: str, + result: BatchTestResult, + ) -> None: + """ + Cache a result for future lookups. + + This is a synchronous method for use from env_step(). + Thread-safe. Uses LRU eviction when cache is full. + + Args: + code_hash: Hash of the submitted code + tests_hash: Hash of the test cases + result: The BatchTestResult to cache + """ + ... diff --git a/src/ludic/envs/code_exec/types.py b/src/ludic/envs/code_exec/types.py new file mode 100644 index 0000000..806559b --- /dev/null +++ b/src/ludic/envs/code_exec/types.py @@ -0,0 +1,258 @@ +""" +Core types for code execution environments. + +These types capture rich metadata about code compilation and execution, +providing RL-relevant signals for reward shaping and analysis. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Dict, List, Optional + + +class SandboxPoolExhaustedError(Exception): + """ + Raised when sandbox pool experiences too many consecutive failures. + + This indicates a systemic issue with sandbox creation/reset that + requires operator intervention. + """ + + pass + + +class CompileStatus(Enum): + """Status of code compilation/syntax checking.""" + + SUCCESS = "success" + SYNTAX_ERROR = "syntax_error" + IMPORT_ERROR = "import_error" + TIMEOUT = "timeout" + UNKNOWN_ERROR = "unknown_error" + + +class RunStatus(Enum): + """Status of code execution.""" + + SUCCESS = "success" + RUNTIME_ERROR = "runtime_error" + TIMEOUT = "timeout" + MEMORY_EXCEEDED = "memory_exceeded" + KILLED = "killed" + NOT_RUN = "not_run" # e.g., skipped due to earlier failure + SANDBOX_ERROR = "sandbox_error" # sandbox crashed, not user code + + +@dataclass +class CompileResult: + """ + Result of compiling/syntax-checking code. + + For Python, this typically uses py_compile or ast.parse to catch + syntax errors before execution. + """ + + status: CompileStatus + error_message: Optional[str] = None + error_line: Optional[int] = None + error_column: Optional[int] = None + duration_ms: float = 0.0 + + @property + def success(self) -> bool: + return self.status == CompileStatus.SUCCESS + + +@dataclass +class ExecutionResult: + """ + Rich result of running code in a sandbox. + + All fields are RL-relevant metadata that can be used for: + - Reward shaping (compile errors vs runtime errors vs wrong answer) + - Curriculum learning (filter by execution characteristics) + - Analysis (understanding failure modes) + + This is the atomic unit returned by sandbox.execute(). + """ + + # Compilation phase + compile_result: CompileResult + + # Execution phase (only meaningful if compilation succeeded) + run_status: Optional[RunStatus] = None + stdout: str = "" + stderr: str = "" + exit_code: Optional[int] = None + return_value: Optional[str] = None # for function-based testing + + # Timing (all in milliseconds) + compile_duration_ms: float = 0.0 + run_duration_ms: float = 0.0 + total_duration_ms: float = 0.0 + + # Resource usage (optional, depends on sandbox implementation) + peak_memory_bytes: Optional[int] = None + cpu_time_ms: Optional[float] = None + + # Cache info + cache_hit: bool = False + cache_key: Optional[str] = None + + @property + def compiled(self) -> bool: + """True if code compiled successfully.""" + return self.compile_result.success + + @property + def succeeded(self) -> bool: + """True if code compiled and ran without errors.""" + return self.compiled and self.run_status == RunStatus.SUCCESS + + @property + def timed_out(self) -> bool: + """True if either compilation or execution timed out.""" + return ( + self.compile_result.status == CompileStatus.TIMEOUT + or self.run_status == RunStatus.TIMEOUT + ) + + +@dataclass +class TestCase: + """ + A single test case. + + The interpretation of `input` and `expected` depends on the CodeRunner: + - stdin/stdout: input is stdin string, expected is stdout string + - function call: input is (args, kwargs), expected is return value + - pytest: input is test code, expected is None (pass/fail from exit code) + """ + + __test__ = False # Prevent pytest from collecting this as a test class + + input: Any + expected: Any + id: str = "" + weight: float = 1.0 # for weighted partial credit + metadata: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class BatchExecutionSpec: + """ + Specification for executing multiple tests in a single batch. + + Used by execute_batch() to run all tests with minimal semaphore acquisitions. + The batch runner receives this as a manifest and executes tests sequentially + inside the container, streaming results back as JSONL. + """ + + code: str + tests: List[TestCase] + compile_first: bool = True + timeout_s: float = 5.0 + stop_on_first_failure: bool = True + + +@dataclass +class TestResult: + """Result of running a single test case.""" + + __test__ = False # Prevent pytest from collecting this as a test class + + test_case: TestCase + passed: bool + actual: Any + execution: ExecutionResult + comparison_details: Optional[str] = None # explains why comparison failed + + @property + def compiled(self) -> bool: + """True if code compiled for this test.""" + return self.execution.compiled + + @property + def ran(self) -> bool: + """True if code actually executed (not skipped).""" + return self.execution.run_status not in (None, RunStatus.NOT_RUN) + + +@dataclass +class BatchTestResult: + """ + Result of running all tests for a code submission. + + Aggregates individual TestResults and provides convenience properties + for computing rewards and analyzing results. + """ + + results: List[TestResult] + code_hash: str + tests_hash: str + + @property + def passed_count(self) -> int: + """Number of tests that passed.""" + return sum(1 for r in self.results if r.passed) + + @property + def total_count(self) -> int: + """Total number of tests.""" + return len(self.results) + + @property + def all_passed(self) -> bool: + """True if all tests passed.""" + return self.passed_count == self.total_count and self.total_count > 0 + + @property + def pass_rate(self) -> float: + """Fraction of tests that passed (0.0 to 1.0).""" + if self.total_count == 0: + return 0.0 + return self.passed_count / self.total_count + + @property + def first_failure(self) -> Optional[TestResult]: + """The first test that failed, or None if all passed.""" + for r in self.results: + if not r.passed: + return r + return None + + @property + def compile_failed(self) -> bool: + """True if code failed to compile (before any tests ran).""" + if not self.results: + return False + # If compilation failed, all tests will have the same compile failure + return not self.results[0].compiled + + @property + def total_execution_ms(self) -> float: + """Total execution time across all tests.""" + return sum(r.execution.total_duration_ms for r in self.results) + + @property + def total_compile_ms(self) -> float: + """Total compilation time (usually same across tests if compiled once).""" + if not self.results: + return 0.0 + # Compilation typically happens once, take max to be safe + return max(r.execution.compile_duration_ms for r in self.results) + + @property + def total_run_ms(self) -> float: + """Total runtime across all tests (excluding compilation).""" + return sum(r.execution.run_duration_ms for r in self.results) + + def get_failures(self) -> List[TestResult]: + """All tests that failed.""" + return [r for r in self.results if not r.passed] + + def get_successes(self) -> List[TestResult]: + """All tests that passed.""" + return [r for r in self.results if r.passed] diff --git a/src/ludic/eval/cli.py b/src/ludic/eval/cli.py index 1deaee2..2fa9061 100644 --- a/src/ludic/eval/cli.py +++ b/src/ludic/eval/cli.py @@ -22,7 +22,7 @@ SamplingParams, ReturnSpec, ) -from ludic.interaction import SingleAgentSyncProtocol +from ludic.interaction import SingleAgentProtocol from ludic.parsers import ParseResult from ludic.training.batching.rollout_engine import RolloutEngine @@ -88,14 +88,14 @@ def build_single_agent_engine( ) -> RolloutEngine: make_ctx = context_factory or (lambda sp: FullDialog(system_prompt=sp)) - def protocol_factory() -> SingleAgentSyncProtocol: + def protocol_factory() -> SingleAgentProtocol: agent = Agent( client=client, model=model, ctx=make_ctx(system_prompt), parser=parser, ) - return SingleAgentSyncProtocol( + return SingleAgentProtocol( agent=agent, stop_on_parse_error=stop_on_parse_error, ) diff --git a/src/ludic/inference/vllm_server.py b/src/ludic/inference/vllm_server.py index a985a21..1f9b5f4 100644 --- a/src/ludic/inference/vllm_server.py +++ b/src/ludic/inference/vllm_server.py @@ -108,9 +108,9 @@ def init_communicator(self, host: str, port: int, world_size: int) -> None: # --- DEBUG: Print internal vLLM parameter names --- # This executes on the worker process. We use Rank 0 to avoid duplicates. if self.pynccl_comm.rank == 0: - print("\n" + "="*60) + print("\n" + "=" * 60) print("🔍 [DEBUG] vLLM Internal Parameter Names (Worker Rank 0)") - print("="*60) + print("=" * 60) try: # Access the underlying torch model model_instance = self.model_runner.model @@ -121,7 +121,7 @@ def init_communicator(self, host: str, port: int, world_size: int) -> None: print(f"Total parameters found: {count}") except Exception as e: print(f"⚠️ Could not print parameter names: {e}") - print("="*60 + "\n") + print("=" * 60 + "\n") # -------------------------------------------------- def update_named_param(self, name: str, dtype: str, shape: Sequence[int]) -> None: @@ -230,7 +230,7 @@ def update_state(self, batch_update: Optional[BatchUpdate]) -> None: self.req_state.pop(ridx, None) # 2) Handle additions - for (req_idx, params, prompt_ids, output_ids) in batch_update.added: + for req_idx, params, prompt_ids, output_ids in batch_update.added: assert isinstance(params, SamplingParams) extra_args = getattr(params, "extra_args", None) @@ -248,7 +248,7 @@ def update_state(self, batch_update: Optional[BatchUpdate]) -> None: } # 3) Handle moves - for (src, dst, direction) in batch_update.moved: + for src, dst, direction in batch_update.moved: if direction == MoveDirectionality.UNIDIRECTIONAL: state = self.req_state.pop(src, None) if state is not None: @@ -371,9 +371,7 @@ async def health() -> dict[str, str]: @app.get("/get_world_size") async def get_world_size() -> dict[str, int]: - return { - "world_size": args.tensor_parallel_size * args.data_parallel_size - } + return {"world_size": args.tensor_parallel_size * args.data_parallel_size} @app.get("/runtime_version") async def runtime_version() -> dict[str, int]: @@ -390,9 +388,7 @@ async def init_communicator(request: Request) -> dict[str, str]: world_size = data.get("world_size") create_background_task( - engine.collective_rpc( - "init_communicator", args=(host, port, world_size) - ) + engine.collective_rpc("init_communicator", args=(host, port, world_size)) ) return {"status": "ok"} @@ -433,18 +429,18 @@ async def update_param_batch(request: Request) -> dict[str, str]: """ data = await request.json() metadata = data.get("metadata", []) # List of {name, dtype, shape} - + # --- DEBUG: Verify what the server received --- - print("\n" + "="*80) + print("\n" + "=" * 80) print(f"📥 [SERVER DEBUG] Received Batch Metadata (Total: {len(metadata)})") - print("="*80) + print("=" * 80) for i, m in enumerate(metadata): # Print only first 10 to avoid spamming logs, or all if short if i < 10: print(f" • {m.get('name')} | {m.get('shape')}") if len(metadata) > 10: - print(f" ... (+{len(metadata)-10} more)") - print("="*80 + "\n") + print(f" ... (+{len(metadata) - 10} more)") + print("=" * 80 + "\n") # ---------------------------------------------- # Check if an explicit version was provided by the Trainer @@ -462,7 +458,7 @@ async def do_update_batch() -> None: # Reset cache and bump version after full batch await engine.reset_prefix_cache() - + global RUNTIME_VERSION async with RUNTIME_VERSION_LOCK: if forced_version is not None: @@ -499,7 +495,7 @@ async def do_update() -> None: await engine.collective_rpc( "update_named_param", args=(name, dtype, shape) ) - + global RUNTIME_VERSION async with RUNTIME_VERSION_LOCK: if requested_version is not None: @@ -578,7 +574,9 @@ def main() -> None: # vLLM can silently override sampling params using the model's Hugging Face # `generation_config` unless `--generation-config vllm` is set. Defaulting # to `vllm` makes Ludic's SamplingParams the source of truth. - if not any(a == "--generation-config" or a.startswith("--generation-config=") for a in argv): + if not any( + a == "--generation-config" or a.startswith("--generation-config=") for a in argv + ): argv = [*argv, "--generation-config", "vllm"] args = parser.parse_args(argv) assert args is not None diff --git a/src/ludic/interaction/__init__.py b/src/ludic/interaction/__init__.py index a9f1cf4..a826ea6 100644 --- a/src/ludic/interaction/__init__.py +++ b/src/ludic/interaction/__init__.py @@ -3,12 +3,12 @@ from .base import InteractionProtocol from .info import merge_step_info from .multi_agent import MultiAgentProtocol -from .single_agent import SingleAgentSyncProtocol +from .single_agent import SingleAgentProtocol from .step_collector import TraceCollector __all__ = [ "InteractionProtocol", - "SingleAgentSyncProtocol", + "SingleAgentProtocol", "MultiAgentProtocol", "TraceCollector", "merge_step_info", diff --git a/src/ludic/interaction/multi_agent.py b/src/ludic/interaction/multi_agent.py index 3992b9c..680f205 100644 --- a/src/ludic/interaction/multi_agent.py +++ b/src/ludic/interaction/multi_agent.py @@ -30,6 +30,12 @@ class MultiAgentProtocol(InteractionProtocol): logged for the failing agent (reward=parse_result.reward, info includes parse_error=True). The failing agent's context is updated with the synthetic observation for the next turn. + + Async environment support: + This protocol does NOT currently support async environments. + It uses the synchronous env.reset() and env.step() methods. + For async multi-agent environments, this protocol would need + async detection similar to SingleAgentProtocol. """ def __init__(self, agents: Dict[str, Agent]): diff --git a/src/ludic/interaction/single_agent.py b/src/ludic/interaction/single_agent.py index d7fe6cc..6cd3f98 100644 --- a/src/ludic/interaction/single_agent.py +++ b/src/ludic/interaction/single_agent.py @@ -1,4 +1,6 @@ from __future__ import annotations + +import inspect from typing import Optional, List from ludic.envs.env import LudicEnv @@ -8,15 +10,48 @@ from .base import InteractionProtocol from .info import merge_step_info -class SingleAgentSyncProtocol(InteractionProtocol): + +def _has_async_env_methods(env: LudicEnv) -> tuple[bool, bool]: """ - Implements the standard single-agent, synchronous interaction loop. - + Detect if environment has async env_reset/env_step methods. + + WARNING: If this returns (True, True), you MUST use the async methods + directly (env.env_reset(), env.env_step()) rather than the sync wrappers + (env.reset(), env.step()). Calling sync wrappers on an async env will + return coroutine objects instead of results. + + This is used to support envs like CodeExecEnv that have async methods + while maintaining backward compatibility with sync envs. + + Returns: + Tuple of (has_async_reset, has_async_step) + """ + has_async_reset = ( + hasattr(env, "env_reset") + and inspect.iscoroutinefunction(env.env_reset) + ) + has_async_step = ( + hasattr(env, "env_step") + and inspect.iscoroutinefunction(env.env_step) + ) + return has_async_reset, has_async_step + +class SingleAgentProtocol(InteractionProtocol): + """ + Implements the standard single-agent interaction loop. + This protocol consumes a LudicEnv but ASSUMES it has exactly one agent and that this agent is active every step. - + It works perfectly with any env inheriting from SingleAgentEnv. + Async env support: + This protocol automatically detects envs with async `env_reset` and + `env_step` methods (e.g., CodeExecEnv). For such envs, the protocol + calls these methods directly and awaits them, bypassing the sync + wrappers in SingleAgentEnv. This provides full backward compatibility + with sync envs while supporting async envs transparently. + Parser failures: If the agent's parser returns ParseResult.action=None, the protocol does not call env.step(). Instead it logs a synthetic Step with @@ -69,15 +104,22 @@ async def run( agent_ids = env.agent_ids if len(agent_ids) != 1: raise ValueError( - f"SingleAgentSyncProtocol requires a LudicEnv with " + f"SingleAgentProtocol requires a LudicEnv with " f"exactly one agent, but found {len(agent_ids)}." ) agent_id = agent_ids[0] + # Check for async env methods (e.g., CodeExecEnv) + has_async_reset, has_async_step = _has_async_env_methods(env) + # 2. --- Reset Env --- - # env.reset() returns a dict - obs_info_dict = env.reset(seed=env_seed) - obs, info = obs_info_dict[agent_id] + # For async envs, call env_reset directly and await it. + # For sync envs, use the standard reset() wrapper. + if has_async_reset: + obs, info = await env.env_reset(seed=env_seed) # type: ignore[union-attr] + else: + obs_info_dict = env.reset(seed=env_seed) + obs, info = obs_info_dict[agent_id] # 3. --- Reset Agent & Feed First Obs --- # Choose system prompt: prefer the context's default if set, else env suggestion. @@ -149,12 +191,14 @@ async def run( parsed_action = parse_result.action parser_reward = parse_result.reward - # Send action to env in the required dict format - actions_dict = {agent_id: parsed_action} - outcomes_dict = env.step(actions_dict) - - # Unwrap the outcome for our agent - env_outcome = outcomes_dict[agent_id] + # For async envs, call env_step directly and await it. + # For sync envs, use the standard step() wrapper. + if has_async_step: + env_outcome = await env.env_step(parsed_action) # type: ignore[union-attr] + else: + actions_dict = {agent_id: parsed_action} + outcomes_dict = env.step(actions_dict) + env_outcome = outcomes_dict[agent_id] # Combine parser and env rewards total_reward = env_outcome.reward + parser_reward diff --git a/src/ludic/training/algorithm.py b/src/ludic/training/algorithm.py index 720d447..2d112fd 100644 --- a/src/ludic/training/algorithm.py +++ b/src/ludic/training/algorithm.py @@ -14,9 +14,19 @@ ClippedSurrogateLoss, TokenClippedSurrogateLoss, CISPOLoss, + SAPOLoss, + GMPOLoss, MaskedCausalLMCrossEntropyLoss, + CompositeLoss, + LossTerm, + TokenKLLoss, +) +from ludic.training.credit_assignment import ( + MonteCarloReturn, + GroupNormalizedReturn, + HybridNormalizedReturn, + ConstantCredit, ) -from ludic.training.credit_assignment import MonteCarloReturn, GroupNormalizedReturn, ConstantCredit Batch = Mapping[str, Tensor] @@ -47,9 +57,19 @@ def compute_loss( self, model: nn.Module, batch: Batch, + *, + cast_logits_to_fp32: bool = False, ) -> tuple[Tensor, Dict[str, Any]]: """ Runs the forward pass once and delegates to the Loss object. + + Args: + model: The trainable model. + batch: Collated batch tensors (input_ids, attention_mask, etc.). + cast_logits_to_fp32: If True, cast logits to FP32 before loss computation. + This improves importance sampling ratio stability for ratio-based + objectives (GRPO, CISPO, etc.) by reducing precision errors in + exp(log_ratio). Recommended by ScaleRL paper (arXiv:2510.13786). """ # --- Run the forward pass --- input_ids = batch["input_ids"] @@ -60,6 +80,10 @@ def compute_loss( ) logits: Logits = outputs.logits + # ScaleRL: FP32 logits prevent IS ratio precision issues in exp(logp_new - logp_old) + if cast_logits_to_fp32: + logits = logits.float() + # Pass the resulting logits to the loss function return self.loss.compute(logits, batch) @@ -282,6 +306,63 @@ def make_grpo( ) +def make_dr_grpo( + *, + group_size: int, + positive_only: bool = False, + clip_eps_low: float = 0.2, + clip_eps_high: float = 0.27, + length_normalize: bool = False, + ratio_clip: Optional[float] = None, + drop_zero_weight: bool = False, + drop_zero_weight_eps: float = 1e-4, + name: str = "dr_grpo", +) -> RLAlgorithm: + """ + Dr. GRPO (GRPO Done Right): removes per-response length normalization and + per-group std normalization while keeping the GRPO-style clipped surrogate. + + - Credit assignment: group-mean baseline only (no std normalization) + - Loss: token-level PPO-style clipped surrogate (Token-TIS) + + This corresponds to the unbiased GRPO variant described in + "Understanding R1-Zero-Like Training: A Critical Perspective". + + Args: + group_size: Number of rollouts per group. + positive_only: If True, clip negative advantages to zero. + clip_eps_low: Lower PPO clipping epsilon for the surrogate objective. + clip_eps_high: Upper PPO clipping epsilon for the surrogate objective. + length_normalize: If True, normalizes by number of action tokens. + This reintroduces length normalization and deviates from Dr. GRPO. + ratio_clip: Optional upper bound C for truncation (min(r, C)). + name: Algorithm name for logging/metrics. + """ + credit_assigner: CreditAssigner = GroupNormalizedReturn( + group_size=group_size, + normalize_adv=False, + positive_only=positive_only, + ) + loss: Loss = TokenClippedSurrogateLoss( + clip_eps_low=clip_eps_low, + clip_eps_high=clip_eps_high, + length_normalize=length_normalize, + ratio_clip=ratio_clip, + ) + preprocess_fns = [] + if drop_zero_weight: + preprocess_fns.append(lambda batch: drop_zero_weight_samples(batch, eps=drop_zero_weight_eps)) + preprocess_fns.append(validate_actor_logps) + preprocess = compose_preprocess(*preprocess_fns) + + return RLAlgorithm( + name=name, + credit_assigner=credit_assigner, + loss=loss, + preprocess=preprocess, + ) + + def make_gspo( *, group_size: int, @@ -400,6 +481,204 @@ def make_cispo( ) +def make_sapo( + *, + group_size: int, + group_normalize_adv: bool = True, + positive_only: bool = False, + tau_pos: float = 1.0, + tau_neg: float = 1.05, + length_normalize: bool = False, + drop_zero_weight: bool = False, + drop_zero_weight_eps: float = 1e-4, + name: str = "sapo", +) -> RLAlgorithm: + """ + SAPO (Soft Adaptive Policy Optimization) preset. + + SAPO replaces hard clipping with a smooth, temperature-controlled sigmoid gate + that adaptively attenuates off-policy updates while preserving learning signals. + The soft gate implements a continuous trust region that is both sequence-coherent + and token-adaptive. + + Core mechanism: + Instead of hard clipping: min(r * A, clip(r, 1-ε, 1+ε) * A) + SAPO uses soft gate: f(r) * A, where f(r) = (4/τ) * σ(τ(r - 1)) + + The sigmoid gate σ(τ(r - 1)) peaks at r=1 (on-policy) and decays smoothly as + r deviates, providing gradual attenuation rather than abrupt cutoff. + + Asymmetric temperatures: + - τ_pos: temperature for positive advantages (increase token logit) + - τ_neg: temperature for negative advantages (decrease token logit) + + Setting τ_neg > τ_pos makes negative gradients decay faster, improving stability. + This is motivated by the observation that negative updates diffuse to many + unsampled tokens in the vocabulary, introducing more noise than positive updates. + + Advantages over hard clipping methods: + - vs GRPO: smooth token-level scaling instead of hard cutoff + - vs GSPO: token-adaptive (preserves signal from near-on-policy tokens even + when sequence has outliers) + - Maintains sequence-level coherence under mild conditions (small steps, + low token variance) + + Args: + group_size: Number of rollouts per group for advantage normalization. + group_normalize_adv: Whether to normalize advantages within each group. + positive_only: If True, clip negative advantages to zero. + tau_pos: Temperature for positive advantages. Default: 1.0 (paper setting). + tau_neg: Temperature for negative advantages. Default: 1.05 (paper setting). + Higher values → faster decay → more conservative. + length_normalize: Whether to normalize by sequence length. + drop_zero_weight: Whether to drop zero-advantage samples. + drop_zero_weight_eps: Epsilon for zero-weight detection. + name: Algorithm name for logging. + + Note: Rollouts must carry `group_id` in their metadata and each group + must have exactly `group_size` members. Use GRPORequestStrategy for + request expansion. + + Reference: "Soft Adaptive Policy Optimization" (arXiv:2511.20347v2) + https://arxiv.org/abs/2511.20347 + + Usage example: + ```python + from ludic.training import make_sapo, GRPORequestStrategy + + # Create SAPO algorithm + algo = make_sapo(group_size=4) + + # Use with GRPO request expansion + request_strategy = GRPORequestStrategy(group_size=4) + ``` + """ + credit_assigner: CreditAssigner = GroupNormalizedReturn( + group_size=group_size, + normalize_adv=group_normalize_adv, + positive_only=positive_only, + ) + loss: Loss = SAPOLoss( + tau_pos=tau_pos, + tau_neg=tau_neg, + length_normalize=length_normalize, + ) + preprocess_fns = [] + if drop_zero_weight: + preprocess_fns.append(lambda batch: drop_zero_weight_samples(batch, eps=drop_zero_weight_eps)) + preprocess_fns.append(validate_actor_logps) + preprocess = compose_preprocess(*preprocess_fns) + + return RLAlgorithm( + name=name, + credit_assigner=credit_assigner, + loss=loss, + preprocess=preprocess, + ) + + +def make_gmpo( + *, + group_size: int, + group_normalize_adv: bool = True, + positive_only: bool = False, + clip_eps_low: float = 0.4, + clip_eps_high: float = 0.4, + length_normalize: bool = True, + ratio_clip: Optional[float] = None, + drop_zero_weight: bool = False, + drop_zero_weight_eps: float = 1e-4, + name: str = "gmpo", +) -> RLAlgorithm: + """ + GMPO (Geometric-Mean Policy Optimization) preset. + + GMPO stabilizes GRPO by using the geometric mean of token-level importance + ratios instead of the arithmetic mean. This makes the objective less sensitive + to outliers and results in more stable policy updates with fewer extreme + importance sampling ratios. + + Key advantages over GRPO: + 1. More robust to outlier tokens (geometric mean vs arithmetic mean) + 2. More stable importance sampling ratios during training + 3. Supports wider clipping ranges (e.g., (e^-0.4, e^0.4) vs (0.8, 1.2)) + 4. Better exploration due to higher entropy maintenance + 5. More stable gradients and lower KL divergence from reference policy + + Objective: + J_GMPO = E[ (∏_t min(ρ_t * A, clip(ρ_t, e^-ε_low, e^ε_high) * A))^(1/|o|) * sgn(A) ] + + where: + - ρ_t = π_new(a_t|s_t) / π_old(a_t|s_t) is the token-level importance ratio + - A is the advantage (group-normalized) + - |o| is the sequence length + - Clipping is performed at the token level in log-space + + Implementation differences from GRPO: + - Uses geometric mean: (∏_t ρ_t)^(1/|o|) instead of (1/|o|) Σ_t ρ_t + - All operations performed in log-space for numerical stability + - Token-level clipping (not sequence-level as in DeepSeek-R1) + - Wider default clipping range: (e^-0.4, e^0.4) ≈ (0.67, 1.49) + + Args: + group_size: Number of rollouts per group for advantage normalization. + group_normalize_adv: Whether to normalize advantages within each group. + Recommended: True (follows GRPO and paper experiments). + positive_only: If True, clip negative advantages to zero. + clip_eps_low: Lower clipping epsilon in log-space. Default 0.4 means + clipping to e^-0.4 ≈ 0.67. Paper uses (e^-0.4, e^0.4). + clip_eps_high: Upper clipping epsilon in log-space. Default 0.4 means + clipping to e^0.4 ≈ 1.49. + length_normalize: Whether to normalize by sequence length (1/|o|). + This is critical for GMPO stability. Default: True. + ratio_clip: Optional upper bound for geometric mean ratio truncation. + drop_zero_weight: Whether to drop zero-advantage samples before training. + drop_zero_weight_eps: Epsilon for zero-weight detection. + name: Algorithm name for logging/metrics. + + Note: Rollouts must carry `group_id` in their metadata and each group + must have exactly `group_size` members. Use GRPORequestStrategy for + request expansion. + + Reference: "GMPO: Geometric-Mean Policy Optimization" (arXiv:2507.20673v3) + https://arxiv.org/abs/2507.20673 + + Usage example: + ```python + from ludic.training import make_gmpo, GRPORequestStrategy + + # Create GMPO algorithm + algo = make_gmpo(group_size=4) + + # Use with GRPO request expansion + request_strategy = GRPORequestStrategy(group_size=4) + ``` + """ + credit_assigner: CreditAssigner = GroupNormalizedReturn( + group_size=group_size, + normalize_adv=group_normalize_adv, + positive_only=positive_only, + ) + loss: Loss = GMPOLoss( + clip_eps_low=clip_eps_low, + clip_eps_high=clip_eps_high, + length_normalize=length_normalize, + ratio_clip=ratio_clip, + ) + preprocess_fns = [] + if drop_zero_weight: + preprocess_fns.append(lambda batch: drop_zero_weight_samples(batch, eps=drop_zero_weight_eps)) + preprocess_fns.append(validate_actor_logps) + preprocess = compose_preprocess(*preprocess_fns) + + return RLAlgorithm( + name=name, + credit_assigner=credit_assigner, + loss=loss, + preprocess=preprocess, + ) + + # --------------------------------------------------------------------------- # SFT (Supervised Fine-Tuning / Behavioral Cloning) # --------------------------------------------------------------------------- @@ -450,3 +729,105 @@ def make_sft( credit_assigner=credit_assigner, loss=loss, ) + + +# --------------------------------------------------------------------------- +# ScaleRL (CISPO + Hybrid Normalization) +# --------------------------------------------------------------------------- + + +def make_scalerl( + *, + group_size: int, + positive_only: bool = False, + clip_eps_low: float = 0.20, + clip_eps_high: float = 0.28, + length_normalize: bool = True, + kl_coeff: float = 0.0, + drop_zero_weight_eps: float = 1e-4, + name: str = "scalerl", +) -> RLAlgorithm: + """ + ScaleRL recipe: CISPO loss + hybrid advantage normalization + zero-weight filtering. + + This combines the key sample-efficiency improvements from the ScaleRL paper: + + 1. **HybridNormalizedReturn**: Group-mean centering + batch-std scaling. + More robust than pure group-level normalization because it avoids + std=0 explosions in low-variance groups (easy prompts). + + 2. **CISPOLoss**: Truncated IS-weight policy gradient that preserves + gradient contributions from rare tokens (crucial for reflective + reasoning behaviors like "Wait", "However", "Recheck"). + + 3. **Drop zero-weight samples**: After credit assignment, drop samples with + near-zero weight to reduce no-op updates. + + 4. **FP32 logits** (via TrainerConfig.cast_logits_to_fp32): + Recommended for IS ratio stability. Not controlled by this preset— + set in TrainerConfig. + + Args: + group_size: Number of rollouts per group (required for credit assignment). + positive_only: If True, clip negative advantages to zero (REINFORCE-only). + clip_eps_low: Lower CISPO clipping bound. Default 0.20 per context-notes.md. + clip_eps_high: Upper CISPO clipping bound. Default 0.28 per context-notes.md. + length_normalize: Whether to normalize by number of action tokens. + kl_coeff: Coefficient for optional token-level KL penalty. + Set > 0 for additional stability. Typical: 0.01-0.1. Default 0.0. + drop_zero_weight_eps: Epsilon for zero-weight sample detection. + name: Algorithm name for logging/metrics. + + Note: Rollouts must carry `group_id` in their metadata and each group + must have exactly `group_size` members. Use GRPORequestStrategy for + request expansion. + + References: + - ScaleRL: arXiv:2510.13786 + - DAPO (zero-weight filtering): arXiv:2503.14476 + - MiniMax-M1 (CISPO): arXiv:2506.13585 + """ + # HybridNormalizedReturn: group-mean baseline + batch-std scaling + credit_assigner: CreditAssigner = HybridNormalizedReturn( + group_size=group_size, + positive_only=positive_only, + ) + + # CISPO loss with asymmetric clipping + cispo_loss: Loss = CISPOLoss( + clip_eps_low=clip_eps_low, + clip_eps_high=clip_eps_high, + length_normalize=length_normalize, + ) + + # Optionally add token-level KL penalty for stability + if kl_coeff > 0: + kl_loss = TokenKLLoss(coeff=kl_coeff, length_normalize=length_normalize) + loss: Loss = CompositeLoss( + terms=[ + LossTerm(name="cispo", loss=cispo_loss, weight=1.0), + LossTerm(name="kl", loss=kl_loss, weight=1.0), + ] + ) + else: + loss = cispo_loss + + # Build preprocessing pipeline (order matters) + preprocess_fns = [] + + # 1. Drop individual zero-weight samples (after credit assignment) + preprocess_fns.append( + lambda batch: drop_zero_weight_samples(batch, eps=drop_zero_weight_eps) + ) + + # 2. Validate actor logprobs (required for CISPO ratio computation) + preprocess_fns.append(validate_actor_logps) + + preprocess = compose_preprocess(*preprocess_fns) + + return RLAlgorithm( + name=name, + credit_assigner=credit_assigner, + loss=loss, + preprocess=preprocess, + ) diff --git a/src/ludic/training/batching/synced_batching.py b/src/ludic/training/batching/synced_batching.py index f4e5377..e55e285 100644 --- a/src/ludic/training/batching/synced_batching.py +++ b/src/ludic/training/batching/synced_batching.py @@ -1,4 +1,6 @@ from __future__ import annotations +import logging +import time from typing import Callable, List, Optional from ludic.training.types import ( @@ -10,6 +12,8 @@ ) from .rollout_engine import RolloutEngine +logger = logging.getLogger(__name__) + class RolloutBatchSource(BatchSource): """ @@ -53,7 +57,13 @@ async def next_batch(self) -> SAWBatch: Pull requests -> Generate (blocking) -> Return Batch. """ requests = self._requests_fn() - return await self._engine.generate_batch( + n_requests = len(requests) + logger.info( + f"Generating batch: {n_requests} rollouts with concurrency={self._concurrency}" + ) + start_time = time.monotonic() + + batch = await self._engine.generate_batch( requests=requests, max_steps=self._max_steps, credit_assigner=self._credit_assigner, @@ -61,3 +71,10 @@ async def next_batch(self) -> SAWBatch: concurrency=self._concurrency, sample_filter=self._sample_filter, ) + + elapsed = time.monotonic() - start_time + logger.info( + f"Batch complete: {len(batch.items)} samples from {n_requests} rollouts " + f"in {elapsed:.1f}s ({n_requests / elapsed:.1f} rollouts/s)" + ) + return batch diff --git a/src/ludic/training/config.py b/src/ludic/training/config.py index a2a82fb..b703e39 100644 --- a/src/ludic/training/config.py +++ b/src/ludic/training/config.py @@ -1,5 +1,17 @@ -from dataclasses import dataclass -from typing import Optional +from dataclasses import dataclass, field +from typing import Any, Optional, Union + + +def _extract_pad_token_id(tokenizer: Any) -> int: + """Extract pad_token_id from a tokenizer, with eos_token_id fallback.""" + if (pad := getattr(tokenizer, "pad_token_id", None)) is not None: + return pad + if (eos := getattr(tokenizer, "eos_token_id", None)) is not None: + return eos + raise ValueError( + "Tokenizer has no pad_token_id or eos_token_id. " + "Set tokenizer.pad_token_id explicitly before passing to TrainerConfig." + ) @dataclass @@ -10,6 +22,16 @@ class TrainerConfig: This is *purely* about optimization / model device / collation. Rollout and batch-generation config live in BatchSource / Orchestrator. + ========================== + Required + ========================== + + - pad_token_id: + Token ID used when padding sequences during SAW collation. + Pass your tokenizer directly and the pad_token_id will be + extracted automatically (with eos_token_id as fallback). + You can also pass an int if you know the exact token ID. + ========================== Model / Optimization ========================== @@ -31,27 +53,27 @@ class TrainerConfig: - max_seq_len: Max token length for any single sample. Trainer raises if exceeded. - + - micro_token_budget: Max padded tokens per micro-batch (roughly batch_size * max_seq_len). Trainer splits macro-batches into micro-batches that fit this budget. Must be >= max_seq_len. - + - sync_every_steps: - Frequency (in macro-steps) at which to push updated policy + Frequency (in macro-steps) at which to push updated policy weights to the Agent's runtime (e.g., vLLM). Set to 0 to disable syncing (e.g., pure offline/local training). - mixed_precision_dtype: - Optional string to configure FSDP's mixed precision policy. + Optional string to configure FSDP's mixed precision policy. Use "bf16" or "fp16". If None, defaults to full precision (fp32). - ========================== - Collation - ========================== - - - pad_token_id: - Used when padding sequences during SAW collation. + - cast_logits_to_fp32: + If True, cast model logits to FP32 before loss computation. + Critical for importance sampling stability in ratio-based RL objectives + (GRPO, CISPO, etc.) where BF16 precision errors compound in exp(log_ratio). + Follows ScaleRL paper's "FP32 at LM head" recommendation. + See: arXiv:2510.13786 (ScaleRL) ========================== Distributed @@ -90,6 +112,9 @@ class TrainerConfig: Optional per-call timeout for eval rollouts. """ + # ----- required (no default) ------------------ + pad_token_id: Union[int, Any] # int or tokenizer-like object + # ----- model / optimization ------------------- model_device: str = "cuda" runtime_device: Optional[str] = None @@ -106,6 +131,7 @@ class TrainerConfig: micro_token_budget: int = 8192 sync_every_steps: int = 1 mixed_precision_dtype: Optional[str] = "bf16" + cast_logits_to_fp32: bool = False # ScaleRL: FP32 logits for IS ratio stability # PipelineRL specific settings max_lag: Optional[int] = None # Drop batches older than N steps @@ -113,12 +139,13 @@ class TrainerConfig: profile_memory: bool = False log_every: int = 1 - # ----- collation ------------------------------ - pad_token_id: int = 0 - # ----- evaluation ----------------------------- eval_at_start: bool = False eval_every_n_steps: Optional[int] = None eval_concurrency: int = 32 eval_max_steps: int = 1 eval_timeout_s: Optional[float] = None + + def __post_init__(self) -> None: + if not isinstance(self.pad_token_id, int): + self.pad_token_id = _extract_pad_token_id(self.pad_token_id) diff --git a/src/ludic/training/credit_assignment.py b/src/ludic/training/credit_assignment.py index c17c351..9418618 100644 --- a/src/ludic/training/credit_assignment.py +++ b/src/ludic/training/credit_assignment.py @@ -97,6 +97,113 @@ def compute( return out +@dataclass +class HybridNormalizedReturn: + """ + ScaleRL-style advantage normalization: group-mean baseline, batch-std scaling. + + Formula: A_i = (R_i - mean(R_group)) / (std(A_batch) + eps) + + This is more robust than pure group-level normalization (GroupNormalizedReturn) + because: + 1. Avoids std=0 explosions in low-variance groups (easy prompts) + 2. Provides consistent advantage scale across diverse prompts + 3. Recommended by ScaleRL and "Tricks or Traps Part I" papers + + The key insight: use group-level *centering* (baseline = group mean) but + batch-level *scaling* (divide by batch std). This combines GRPO's per-prompt + baseline with robust global scaling. + + Contract: + - Rollouts must have `group_id` in `rollout.meta["request_meta"]["group_id"]`. + - Each group must have exactly `group_size` rollouts. + - Raises ValueError if either condition is violated. + + Args: + group_size: Number of rollouts per group. + eps: Small constant for numerical stability in std division. + positive_only: If True, clip negative advantages to 0. + + Reference: ScaleRL (arXiv:2510.13786), Tricks or Traps Part I (arXiv:2508.08221) + """ + + group_size: int + eps: float = 1e-8 + positive_only: bool = False + + def __post_init__(self): + if self.group_size <= 0: + raise ValueError(f"group_size must be positive, got {self.group_size}") + + def compute( + self, + rollouts: List[Rollout], + ) -> Dict[RolloutStepKey, float]: + + out: Dict[RolloutStepKey, float] = {} + + # Group by group_id from request meta + groups: Dict[str, List[Rollout]] = defaultdict(list) + for r in rollouts: + group_id = r.meta.get("request_meta", {}).get("group_id") + if group_id is None: + raise ValueError( + f"Rollout {r.id} missing group_id in meta['request_meta']. " + "HybridNormalizedReturn requires each rollout to have a group_id." + ) + groups[group_id].append(r) + + # Phase 1: Compute group-centered advantages (A_i = R_i - mean(R_group)) + # Store (rollout, advantage) pairs for batch-level normalization + all_advantages: List[float] = [] + rollout_advantages: List[tuple[Rollout, float]] = [] + + for group_id, group_rollouts in groups.items(): + # Validate group size + actual_size = len(group_rollouts) + if actual_size != self.group_size: + raise ValueError( + f"Group size mismatch for group_id={group_id}: " + f"expected {self.group_size}, got {actual_size}." + ) + + # Get total reward for each rollout in the group + rewards = torch.tensor( + [r.total_reward for r in group_rollouts], + dtype=torch.float32, + ) + + # Group-level centering: A_i = R_i - mean(R_group) + baseline = rewards.mean() + advantages = rewards - baseline + + for i, r in enumerate(group_rollouts): + adv = advantages[i].item() + all_advantages.append(adv) + rollout_advantages.append((r, adv)) + + # Phase 2: Batch-level std normalization + if len(all_advantages) == 0: + return out + + all_adv_tensor = torch.tensor(all_advantages, dtype=torch.float32) + batch_std = all_adv_tensor.std(unbiased=False) + + # Normalize all advantages by batch std + for rollout, raw_adv in rollout_advantages: + adv = raw_adv / (batch_std.item() + self.eps) + + if self.positive_only: + adv = max(adv, 0.0) + + # Assign same advantage to all steps in the rollout + for step in rollout.steps: + key: RolloutStepKey = (rollout.id, step.index) + out[key] = adv + + return out + + @dataclass class MonteCarloReturn: """ diff --git a/src/ludic/training/hardware.py b/src/ludic/training/hardware.py new file mode 100644 index 0000000..e1cad63 --- /dev/null +++ b/src/ludic/training/hardware.py @@ -0,0 +1,251 @@ +""" +GPU hardware detection and Flash Attention configuration utilities. + +This module provides utilities for: +- Detecting GPU architecture (Hopper, Ampere, etc.) +- Selecting optimal attention implementation based on hardware +- Configuring PyTorch SDPA backends for Flash Attention + +Usage: + from ludic.training.hardware import configure_flash_attention + + # In training script, after device detection: + attn_impl = configure_flash_attention(device="cuda", disable_flash_attn=False) + model = AutoModelForCausalLM.from_pretrained(..., attn_implementation=attn_impl) +""" + +from __future__ import annotations + +import logging +from typing import Literal, Optional + +import torch + +logger = logging.getLogger(__name__) + +# GPU architecture compute capability mapping +# See: https://developer.nvidia.com/cuda-gpus +GPU_ARCHITECTURES = { + (9, 0): "hopper", # H100, H200, GH200 + (8, 9): "ada", # RTX 4090, L40 + (8, 6): "ampere", # RTX 3090, A10 + (8, 0): "ampere", # A100 + (7, 5): "turing", # RTX 2080, T4 + (7, 0): "volta", # V100 +} + +AttentionImpl = Literal["flash_attention_3", "flash_attention_2", "sdpa", "eager"] + + +def detect_gpu_architecture() -> Optional[str]: + """ + Detect the GPU architecture from CUDA compute capability. + + Returns: + Architecture name: "hopper", "ampere", "ada", "turing", "volta", or None + if no CUDA GPU is available. + """ + if not torch.cuda.is_available(): + return None + + try: + capability = torch.cuda.get_device_capability() + arch = GPU_ARCHITECTURES.get(capability) + if arch is None: + # Unknown architecture, try to infer from major version + major = capability[0] + if major >= 9: + arch = "hopper" + elif major >= 8: + arch = "ampere" + else: + arch = "older" + return arch + except Exception as e: + logger.warning(f"Failed to detect GPU architecture: {e}") + return None + + +def get_cuda_version() -> Optional[tuple[int, int]]: + """ + Get the CUDA runtime version. + + Returns: + Tuple of (major, minor) version, or None if CUDA unavailable. + """ + if not torch.cuda.is_available(): + return None + + try: + version = torch.version.cuda + if version is None: + return None + parts = version.split(".") + return (int(parts[0]), int(parts[1])) + except Exception as e: + logger.warning(f"Failed to get CUDA version: {e}") + return None + + +def _check_flash_attn_3_available() -> bool: + """ + Check if Flash Attention 3 is available for HuggingFace Transformers. + + HuggingFace Transformers checks for flash_attention_3 support via: + importlib.util.find_spec("flash_attn_3") + + This requires either: + 1. The flash_attn_3 package installed (pip install flash_attn_3) + 2. Building flash-attn from the hopper/ subdirectory + 3. Using HuggingFace 'kernels' package (pip install kernels) + + Returns True only if HuggingFace will accept flash_attention_3. + """ + import importlib.util + + # Check what HuggingFace Transformers actually checks + if importlib.util.find_spec("flash_attn_3") is not None: + logger.info("flash_attn_3 package found - FA3 available") + return True + + # Also check for flash_attn_interface (alternative FA3 installation) + if importlib.util.find_spec("flash_attn_interface") is not None: + logger.info("flash_attn_interface found - FA3 may be available") + # Note: This might not work with all HF Transformers versions + # as they specifically check for flash_attn_3, not flash_attn_interface + return False # Be conservative - HF checks for flash_attn_3 specifically + + logger.debug("FA3 not available (flash_attn_3 package not found)") + return False + + +def get_optimal_attention_impl( + *, + disable_flash_attn: bool = False, +) -> AttentionImpl: + """ + Determine the optimal attention implementation for the current hardware. + + Selection logic: + - Hopper (H100/H200) + CUDA >= 12.3 + flash-attn >= 2.7: flash_attention_3 + - Ampere/Ada + CUDA >= 11.6 + flash-attn installed: flash_attention_2 + - Otherwise: sdpa (PyTorch native, still uses flash kernels when possible) + + Args: + disable_flash_attn: If True, skip flash attention and use SDPA. + + Returns: + Attention implementation string for HuggingFace models: + "flash_attention_3", "flash_attention_2", "sdpa", or "eager" + """ + if disable_flash_attn: + logger.info("Flash Attention disabled by user request, using SDPA") + return "sdpa" + + arch = detect_gpu_architecture() + cuda_version = get_cuda_version() + + # Check if flash_attn is available + try: + import flash_attn + flash_attn_available = True + flash_attn_version = getattr(flash_attn, "__version__", "unknown") + except ImportError: + flash_attn_available = False + flash_attn_version = None + + if not flash_attn_available: + logger.info(f"flash-attn not installed, using SDPA (arch={arch})") + return "sdpa" + + # Flash Attention 3: Hopper-only (H100/H200) with CUDA >= 12.3 + # Achieves 1.5-2x speedup over FA2, 75% H100 utilization + # Ref: https://arxiv.org/abs/2407.08608 + if arch == "hopper" and cuda_version and cuda_version >= (12, 3): + if _check_flash_attn_3_available(): + logger.info( + f"Using flash_attention_3 (arch={arch}, cuda={cuda_version}, " + f"flash_attn={flash_attn_version})" + ) + return "flash_attention_3" + + # Flash Attention 2: Ampere+ with CUDA >= 11.6 + if arch in ("hopper", "ampere", "ada") and cuda_version and cuda_version >= (11, 6): + logger.info( + f"Using flash_attention_2 (arch={arch}, cuda={cuda_version}, " + f"flash_attn={flash_attn_version})" + ) + return "flash_attention_2" + + # Fallback to SDPA (PyTorch native, also uses flash kernels when possible) + logger.info(f"Using SDPA (arch={arch}, cuda={cuda_version})") + return "sdpa" + + +def configure_flash_attention( + device: str = "cuda", + *, + disable_flash_attn: bool = False, +) -> AttentionImpl: + """ + Configure Flash Attention for optimal performance. + + This function: + 1. Enables PyTorch's Flash SDP backend (if available) + 2. Returns the optimal attention implementation for HuggingFace models + + Args: + device: Target device ("cuda" or "cpu") + disable_flash_attn: If True, disable flash attention entirely. + + Returns: + Attention implementation string to pass to model.from_pretrained(). + + Example: + attn_impl = configure_flash_attention("cuda") + model = AutoModelForCausalLM.from_pretrained( + model_name, + attn_implementation=attn_impl, + ) + """ + if device != "cuda" or not torch.cuda.is_available(): + logger.info("No CUDA device, using eager attention") + return "eager" + + # Enable Flash SDP backend in PyTorch (uses flash kernels for F.scaled_dot_product_attention) + if not disable_flash_attn: + try: + torch.backends.cuda.enable_flash_sdp(True) + logger.debug("Enabled torch.backends.cuda.flash_sdp") + except Exception as e: + logger.warning(f"Could not enable flash_sdp: {e}") + + return get_optimal_attention_impl(disable_flash_attn=disable_flash_attn) + + +def log_hardware_info() -> None: + """Log GPU hardware information for debugging.""" + if not torch.cuda.is_available(): + logger.info("No CUDA GPU available") + return + + try: + device_name = torch.cuda.get_device_name() + capability = torch.cuda.get_device_capability() + arch = detect_gpu_architecture() + cuda_version = get_cuda_version() + + logger.info( + f"GPU: {device_name} (sm_{capability[0]}{capability[1]}, {arch}), " + f"CUDA: {cuda_version[0]}.{cuda_version[1] if cuda_version else 'N/A'}" + ) + + # Check flash_attn + try: + import flash_attn + logger.info(f"flash-attn version: {flash_attn.__version__}") + except ImportError: + logger.info("flash-attn: not installed") + + except Exception as e: + logger.warning(f"Could not log hardware info: {e}") diff --git a/src/ludic/training/loss.py b/src/ludic/training/loss.py index 18fe078..ebc708f 100644 --- a/src/ludic/training/loss.py +++ b/src/ludic/training/loss.py @@ -1,6 +1,8 @@ from __future__ import annotations from dataclasses import dataclass +from contextlib import contextmanager +from contextvars import ContextVar import logging import os from beartype.typing import Any, Dict, Mapping, Protocol, Tuple, List, Optional @@ -31,6 +33,177 @@ def _no_op(fn): ) +# --------------------------------------------------------------------------- +# Shared context for memory-efficient loss composition +# --------------------------------------------------------------------------- + + +_shared_context_var: ContextVar[Optional["SharedContext"]] = ContextVar( + "ludic_shared_context", + default=None, +) + + +def _get_shared_context( + logits: Logits, + *, + batch: Optional[Batch] = None, + input_ids: Optional[TokenIds] = None, + action_mask: Optional[Mask] = None, +) -> Optional["SharedContext"]: + shared = _shared_context_var.get() + if shared is None: + return None + if shared.logits is not logits: + return None + if batch is not None and shared.batch is not batch: + return None + if input_ids is not None and shared.batch.get("input_ids") is not input_ids: + return None + if action_mask is not None and shared.batch.get("action_mask") is not action_mask: + return None + return shared + + +@contextmanager +def _use_shared_context(shared: "SharedContext"): + token = _shared_context_var.set(shared) + try: + yield + finally: + _shared_context_var.reset(token) + + +class SharedContext: + """ + Lazy-computed shared tensors for memory-efficient loss composition. + + When multiple losses are combined via CompositeLoss, each typically needs + the same expensive intermediate tensors (e.g., token_logp from log_softmax). + Without sharing, each loss computes these independently, creating separate + autograd graphs that store duplicate [B, T, V] activations for backward. + + SharedContext solves this by computing expensive tensors ONCE on first access + and caching them for subsequent uses. All losses receive the same tensor + objects, sharing a single autograd graph. + + Memory savings example (7B model, V=32K, B=8, T=4096): + - Without sharing (2 losses): 2× [B, T, V] ≈ 4GB activations + - With sharing (2 losses): 1× [B, T, V] ≈ 2GB activations + + Usage: + # CompositeLoss installs a SharedContext so helpers can reuse cached tensors. + with _use_shared_context(SharedContext(logits, batch)): + token_logp = compute_token_logp(logits, batch["input_ids"]) + + Note: Properties that depend on batch["actor_logps"] will raise KeyError + if that key is missing. This is intentional - not all loss combinations + need actor logprobs. + """ + + __slots__ = ("logits", "batch", "_cache") + + def __init__(self, logits: Logits, batch: Batch) -> None: + self.logits = logits + self.batch = batch + self._cache: Dict[str, Tensor] = {} + + @property + def input_ids(self) -> TokenIds: + """Token IDs from batch (not cached, just a convenience accessor).""" + return self.batch["input_ids"] + + @property + def action_mask(self) -> Mask: + """Action mask from batch (not cached, just a convenience accessor).""" + return self.batch["action_mask"] + + @property + def token_logp(self) -> Float[Tensor, "B T-1"]: + """ + Per-token log probabilities: log π(a_t|s_t) for each position. + + THIS IS THE EXPENSIVE OPERATION - calls selective_log_softmax which + requires storing [B, T, V] activations for backward. Caching this + is the primary memory optimization. + """ + if "token_logp" not in self._cache: + self._cache["token_logp"] = _compute_token_logp_raw(self.logits, self.input_ids) + return self._cache["token_logp"] + + @property + def token_mask(self) -> Float[Tensor, "B T-1"]: + """Action mask aligned with token_logp (shifted by 1 for next-token prediction).""" + if "token_mask" not in self._cache: + self._cache["token_mask"] = self.action_mask[:, 1:].to( + self.token_logp.dtype + ) + return self._cache["token_mask"] + + @property + def token_counts(self) -> Float[Tensor, "B"]: + """Number of action tokens per sample (for length normalization).""" + if "token_counts" not in self._cache: + self._cache["token_counts"] = self.token_mask.sum(dim=-1).clamp(min=1.0) + return self._cache["token_counts"] + + @property + def actor_logps_shifted(self) -> Float[Tensor, "B T-1"]: + """ + Behavior policy log probs aligned with token_logp. + + Raises: + KeyError: If batch["actor_logps"] is not present. + """ + if "actor_logps_shifted" not in self._cache: + if "actor_logps" not in self.batch: + raise KeyError( + "SharedContext.actor_logps_shifted requires batch['actor_logps']. " + "Ensure your rollouts include actor_logps for ratio-based objectives." + ) + actor_logps = self.batch["actor_logps"] + if actor_logps.shape != self.input_ids.shape: + raise ValueError( + f"actor_logps shape {tuple(actor_logps.shape)} does not match input_ids " + f"{tuple(self.input_ids.shape)}." + ) + self._cache["actor_logps_shifted"] = actor_logps[:, 1:] + return self._cache["actor_logps_shifted"] + + @property + def log_ratio(self) -> Float[Tensor, "B T-1"]: + """Log importance ratio: log(π_new/π_old) per token.""" + if "log_ratio" not in self._cache: + self._cache["log_ratio"] = self.token_logp - self.actor_logps_shifted + return self._cache["log_ratio"] + + @property + def ratio(self) -> Float[Tensor, "B T-1"]: + """Importance ratio: π_new/π_old per token.""" + if "ratio" not in self._cache: + self._cache["ratio"] = torch.exp(self.log_ratio) + return self._cache["ratio"] + + def logp_action(self, *, length_normalize: bool = False) -> Float[Tensor, "B"]: + """ + Sequence-level log probability (sum over action tokens). + + Unlike token_logp, this is a cheap derivation that doesn't require + additional [B, T, V] storage. The length_normalize flag controls + whether to divide by number of action tokens. + + Args: + length_normalize: If True, return mean log prob instead of sum. + + Returns: + [B] tensor of per-sample log probabilities. + """ + masked_logp = (self.token_logp * self.token_mask).sum(dim=-1) + if length_normalize: + return masked_logp / self.token_counts + return masked_logp + + class Loss(Protocol): """ Generic loss: given model outputs (logits) and a collated batch, return @@ -46,11 +219,13 @@ def compute(self, logits: Logits, batch: Batch) -> Tuple[Tensor, Dict[str, Tenso # We define this as a standalone helper so torch.compile can cache it cleanly. # dynamic=True is critical for varying sequence lengths (preventing recompilation). @jaxtyped(typechecker=typechecker) -@torch.compile(dynamic=True) -def selective_log_softmax(logits: Logits, index: TokenIds) -> Float[Tensor, "B T"]: +def _selective_log_softmax_impl( + logits: Logits, + index: TokenIds, +) -> Float[Tensor, "B T"]: """ Fused kernel for log_softmax + gather. - + Inductor (torch.compile) generates a kernel that computes the log_softmax normalization term and selects the target token in a single pass. This avoids materializing the massive [B, T, V] probability tensor in VRAM. @@ -59,8 +234,32 @@ def selective_log_softmax(logits: Logits, index: TokenIds) -> Float[Tensor, "B T logprobs = logits.log_softmax(dim=-1) return torch.gather(logprobs, dim=-1, index=index.unsqueeze(-1)).squeeze(-1) + +_USE_TORCH_COMPILE = os.getenv("LUDIC_DISABLE_TORCH_COMPILE", "0") != "1" +_USE_COMPILED_SELECTIVE_LOG_SOFTMAX = _USE_TORCH_COMPILE +if _USE_TORCH_COMPILE: + _selective_log_softmax_compiled = torch.compile( + _selective_log_softmax_impl, dynamic=True + ) +else: + _selective_log_softmax_compiled = _selective_log_softmax_impl + + +def selective_log_softmax(logits: Logits, index: TokenIds) -> Float[Tensor, "B T"]: + global _USE_COMPILED_SELECTIVE_LOG_SOFTMAX + if _USE_COMPILED_SELECTIVE_LOG_SOFTMAX: + try: + return _selective_log_softmax_compiled(logits, index) + except Exception as exc: + logger.warning( + "torch.compile failed for selective_log_softmax, falling back to eager: %s", + exc, + ) + _USE_COMPILED_SELECTIVE_LOG_SOFTMAX = False + return _selective_log_softmax_impl(logits, index) + @jaxtyped(typechecker=typechecker) -def compute_logp_action( +def _compute_logp_action_raw( logits: Logits, input_ids: TokenIds, action_mask: Mask, @@ -80,16 +279,18 @@ def compute_logp_action( """ if logits.ndim != 3: raise ValueError(f"Expected logits [B, T, V], got {tuple(logits.shape)}") - + if input_ids.shape != logits.shape[:2]: - raise ValueError(f"Shape mismatch: input_ids {input_ids.shape} vs logits {logits.shape}") + raise ValueError( + f"Shape mismatch: input_ids {input_ids.shape} vs logits {logits.shape}" + ) # Shift for causal LM: logits[t] predicts input_ids[t+1] if logits.size(1) < 2: raise ValueError("Sequence too short to compute next-token logprobs.") - logits_shifted = logits[:, :-1, :] # [B, T-1, V] - target_ids = input_ids[:, 1:] # [B, T-1] - action_mask_shifted = action_mask[:, 1:] # [B, T-1] + logits_shifted = logits[:, :-1, :] # [B, T-1, V] + target_ids = input_ids[:, 1:] # [B, T-1] + action_mask_shifted = action_mask[:, 1:] # [B, T-1] # Use the compiled fused kernel on aligned targets token_logp = selective_log_softmax(logits_shifted, target_ids) @@ -106,7 +307,30 @@ def compute_logp_action( @jaxtyped(typechecker=typechecker) -def compute_token_logp( +def compute_logp_action( + logits: Logits, + input_ids: TokenIds, + action_mask: Mask, + *, + length_normalize: bool = False, +) -> Weights: + shared = _get_shared_context( + logits, + input_ids=input_ids, + action_mask=action_mask, + ) + if shared is not None: + return shared.logp_action(length_normalize=length_normalize) + return _compute_logp_action_raw( + logits, + input_ids, + action_mask, + length_normalize=length_normalize, + ) + + +@jaxtyped(typechecker=typechecker) +def _compute_token_logp_raw( logits: Logits, input_ids: TokenIds, ) -> Float[Tensor, "B T-1"]: @@ -128,6 +352,19 @@ def compute_token_logp( return selective_log_softmax(logits_shifted, target_ids) +@jaxtyped(typechecker=typechecker) +def compute_token_logp( + logits: Logits, + input_ids: TokenIds, +) -> Float[Tensor, "B T-1"]: + shared = _get_shared_context(logits, input_ids=input_ids) + if shared is not None: + if "token_logp" not in shared._cache: + shared._cache["token_logp"] = _compute_token_logp_raw(logits, input_ids) + return shared._cache["token_logp"] + return _compute_token_logp_raw(logits, input_ids) + + # --------------------------------------------------------------------------- # REINFORCE family # --------------------------------------------------------------------------- @@ -155,7 +392,11 @@ class ReinforceLoss: old_logp_key: str = "old_logp_action" @jaxtyped(typechecker=typechecker) - def compute(self, logits: Logits, batch: Batch) -> Tuple[Tensor, Dict[str, Any]]: + def compute( + self, + logits: Logits, + batch: Batch, + ) -> Tuple[Tensor, Dict[str, Any]]: input_ids = batch["input_ids"] # [B, T] action_mask = batch["action_mask"] # [B, T] advantages = batch["weight"] # [B] @@ -166,10 +407,11 @@ def compute(self, logits: Logits, batch: Batch) -> Tuple[Tensor, Dict[str, Any]] logp_action = compute_logp_action( logits, input_ids, action_mask, length_normalize=self.length_normalize ) # [B] + token_counts = action_mask[:, 1:].sum(dim=-1).clamp(min=1.0) old_logp = batch[self.old_logp_key] # [B] if self.length_normalize: - lengths = action_mask[:, 1:].to(old_logp.dtype).sum(dim=-1).clamp(min=1.0) + lengths = token_counts.to(old_logp.dtype) old_logp = old_logp / lengths log_ratio = logp_action - old_logp @@ -214,7 +456,11 @@ class MaskedCausalLMCrossEntropyLoss: length_normalize: bool = True @jaxtyped(typechecker=typechecker) - def compute(self, logits: Logits, batch: Batch) -> Tuple[Tensor, Dict[str, Any]]: + def compute( + self, + logits: Logits, + batch: Batch, + ) -> Tuple[Tensor, Dict[str, Any]]: input_ids = batch["input_ids"] # [B, T] action_mask = batch["action_mask"] # [B, T] weights = batch.get("weight") @@ -222,7 +468,9 @@ def compute(self, logits: Logits, batch: Batch) -> Tuple[Tensor, Dict[str, Any]] if logits.ndim != 3: raise ValueError(f"Expected logits [B, T, V], got {tuple(logits.shape)}") if input_ids.shape != logits.shape[:2]: - raise ValueError(f"Shape mismatch: input_ids {input_ids.shape} vs logits {logits.shape}") + raise ValueError( + f"Shape mismatch: input_ids {input_ids.shape} vs logits {logits.shape}" + ) if logits.size(1) < 2: raise ValueError("Sequence too short to compute next-token loss.") @@ -281,21 +529,26 @@ class ReinforceBaselineLoss: old_logp_key: str = "old_logp_action" @jaxtyped(typechecker=typechecker) - def compute(self, logits: Logits, batch: Batch) -> Tuple[Tensor, Dict[str, Any]]: - input_ids = batch["input_ids"] + def compute( + self, + logits: Logits, + batch: Batch, + ) -> Tuple[Tensor, Dict[str, Any]]: action_mask = batch["action_mask"] adv_raw = batch["weight"] # [B] if self.old_logp_key not in batch: raise KeyError(f"ReinforceBaselineLoss requires '{self.old_logp_key}' in batch.") + input_ids = batch["input_ids"] logp_action = compute_logp_action( logits, input_ids, action_mask, length_normalize=self.length_normalize ) # [B] + token_counts = action_mask[:, 1:].sum(dim=-1).clamp(min=1.0) old_logp = batch[self.old_logp_key] # [B] if self.length_normalize: - lengths = action_mask[:, 1:].to(old_logp.dtype).sum(dim=-1).clamp(min=1.0) + lengths = token_counts.to(old_logp.dtype) old_logp = old_logp / lengths log_ratio = logp_action - old_logp @@ -362,22 +615,30 @@ def __post_init__(self) -> None: raise ValueError(f"ratio_clip must be positive, got {self.ratio_clip}") @jaxtyped(typechecker=typechecker) - def compute(self, logits: Logits, batch: Batch) -> Tuple[Tensor, Dict[str, Any]]: - input_ids = batch["input_ids"] + def compute( + self, + logits: Logits, + batch: Batch, + ) -> Tuple[Tensor, Dict[str, Any]]: action_mask = batch["action_mask"] advantages = batch["weight"] # [B] if self.old_logp_key not in batch: - raise KeyError(f"ClippedSurrogateLoss requires '{self.old_logp_key}' in batch.") + raise KeyError( + f"ClippedSurrogateLoss requires '{self.old_logp_key}' in batch." + ) + input_ids = batch["input_ids"] logp_action = compute_logp_action( logits, input_ids, action_mask, length_normalize=self.length_normalize, ) # [B] + token_counts = action_mask[:, 1:].sum(dim=-1).clamp(min=1.0) + old_logp = batch[self.old_logp_key] # [B] if self.length_normalize: - lengths = action_mask[:, 1:].to(old_logp.dtype).sum(dim=-1).clamp(min=1.0) + lengths = token_counts.to(old_logp.dtype) old_logp = old_logp / lengths log_ratio = logp_action - old_logp @@ -395,9 +656,21 @@ def compute(self, logits: Logits, batch: Batch) -> Tuple[Tensor, Dict[str, Any]] obj = torch.min(unclipped, clipped) loss = -obj.mean() - ppo_clip_frac = ( - (ratio > 1.0 + self.clip_eps_high) | (ratio < 1.0 - self.clip_eps_low) - ).float().mean() + # Token-weighted clip fraction: counts tokens in sequences where the + # clipped branch is active (sequence-level GSPO-style metric). + token_mask = action_mask[:, 1:].to(dtype=ratio.dtype) + token_counts = token_mask.sum(dim=-1).clamp(min=1.0) + adv_pos = advantages >= 0 + seq_clipped = torch.where( + adv_pos, + ratio > 1.0 + self.clip_eps_high, + ratio < 1.0 - self.clip_eps_low, + ) + total_tokens = token_counts.sum() + if total_tokens > 0: + ppo_clip_frac = (seq_clipped.to(token_counts.dtype) * token_counts).sum() / total_tokens + else: + ppo_clip_frac = torch.zeros((), device=ratio.device, dtype=ratio.dtype) if self.ratio_clip is not None: ratio_clip_frac = (ratio >= self.ratio_clip).float().mean() else: @@ -453,14 +726,18 @@ def __post_init__(self) -> None: raise ValueError(f"clip_eps_high must be non-negative, got {self.clip_eps_high}") @jaxtyped(typechecker=typechecker) - def compute(self, logits: Logits, batch: Batch) -> Tuple[Tensor, Dict[str, Any]]: - input_ids = batch["input_ids"] + def compute( + self, + logits: Logits, + batch: Batch, + ) -> Tuple[Tensor, Dict[str, Any]]: action_mask = batch["action_mask"] advantages = batch["weight"] # [B] if "actor_logps" not in batch: raise KeyError("CISPOLoss requires batch['actor_logps'] for importance sampling.") + input_ids = batch["input_ids"] actor_logps = batch["actor_logps"] # [B, T] if actor_logps.shape != input_ids.shape: raise ValueError( @@ -565,20 +842,23 @@ def __post_init__(self) -> None: raise ValueError(f"ratio_clip must be positive, got {self.ratio_clip}") @jaxtyped(typechecker=typechecker) - def compute(self, logits: Logits, batch: Batch) -> Tuple[Tensor, Dict[str, Any]]: - input_ids = batch["input_ids"] + def compute( + self, + logits: Logits, + batch: Batch, + ) -> Tuple[Tensor, Dict[str, Any]]: action_mask = batch["action_mask"] advantages = batch["weight"] if "actor_logps" not in batch: raise KeyError("TokenClippedSurrogateLoss requires batch['actor_logps'] for token IS.") + input_ids = batch["input_ids"] actor_logps = batch["actor_logps"] if actor_logps.shape != input_ids.shape: raise ValueError( f"actor_logps shape {tuple(actor_logps.shape)} does not match input_ids " f"{tuple(input_ids.shape)}." ) - token_logp = compute_token_logp(logits, input_ids) # [B, T-1] token_mask = action_mask[:, 1:].to(token_logp.dtype) token_counts = token_mask.sum(dim=-1).clamp(min=1.0) @@ -640,11 +920,431 @@ def compute(self, logits: Logits, batch: Batch) -> Tuple[Tensor, Dict[str, Any]] return loss, stats +@dataclass +class SAPOLoss: + """ + SAPO (Soft Adaptive Policy Optimization) loss. + + SAPO replaces hard clipping with a smooth, temperature-controlled sigmoid gate + that adaptively attenuates off-policy updates while preserving useful learning + signals. Unlike hard clipping (GRPO) or sequence-level gates (GSPO), SAPO + applies a soft trust region at the token level that naturally yields sequence-level + coherence under mild conditions. + + Core idea: + Instead of hard clipping: min(r * A, clip(r, 1-ε, 1+ε) * A) + SAPO uses soft gate: f(r) * A, where f(r) = (4/τ) * σ(τ(r - 1)) + + The sigmoid gate σ(τ(r - 1)) peaks at r=1 and decays smoothly as r deviates, + implementing a continuous trust region. The temperature τ controls decay rate: + larger τ → faster decay → more conservative updates. + + Asymmetric temperatures: + - τ_pos for positive advantages (token logit should increase) + - τ_neg for negative advantages (token logit should decrease) + + Setting τ_neg > τ_pos makes negative gradients decay faster, improving stability. + Rationale: Negative updates diffuse to many unsampled tokens in a large vocabulary, + introducing more noise than positive updates which focus on the sampled token. + + Objective: + J_SAPO = E[ (1/|o|) Σ_t f(ρ_t) * A ] + where f(r) = (4/τ) * σ(τ(r - 1)) + and τ = τ_pos if A > 0 else τ_neg + + Gradient weight (from differentiating f): + w(r) = 4 * p(r) * (1 - p(r)), where p(r) = σ(τ(r - 1)) + This peaks at r=1 with value 1 and decays smoothly. + + Connection to other methods: + - Under mild conditions (small steps, low token variance), SAPO reduces to + sequence-level optimization like GSPO but with smooth gating + - Compared to GRPO's hard token clipping, SAPO provides smooth scaling + - Compared to GSPO's sequence-level hard clipping, SAPO is token-adaptive + + Expects: + - batch["weight"]: A (advantages) [B] + - batch["actor_logps"]: token logps under behavior policy [B, T] + - input_ids / attention_mask / action_mask for π_new + + Reference: "Soft Adaptive Policy Optimization" (arXiv:2511.20347v2) + https://arxiv.org/abs/2511.20347 + """ + + tau_pos: float = 1.0 # Temperature for positive advantages + tau_neg: float = 1.05 # Temperature for negative advantages (higher for stability) + length_normalize: bool = False # Normalize by sequence length + + def __post_init__(self) -> None: + if self.tau_pos <= 0 or self.tau_neg <= 0: + raise ValueError( + f"tau_pos/tau_neg must be positive, got {self.tau_pos}, {self.tau_neg}" + ) + + @jaxtyped(typechecker=typechecker) + def compute( + self, + logits: Logits, + batch: Batch, + ) -> Tuple[Tensor, Dict[str, Any]]: + action_mask = batch["action_mask"] + advantages = batch["weight"] # [B] + + if "actor_logps" not in batch: + raise KeyError("SAPOLoss requires batch['actor_logps'] for token IS.") + + input_ids = batch["input_ids"] + actor_logps = batch["actor_logps"] + if actor_logps.shape != input_ids.shape: + raise ValueError( + f"actor_logps shape {tuple(actor_logps.shape)} does not match input_ids " + f"{tuple(input_ids.shape)}." + ) + + # Compute token-level log probabilities + token_logp = compute_token_logp(logits, input_ids) # [B, T-1] + token_mask = action_mask[:, 1:].to(token_logp.dtype) # [B, T-1] + token_counts = token_mask.sum(dim=-1).clamp(min=1.0) # [B] + actor_logps_shifted = actor_logps[:, 1:] # [B, T-1] + + # Compute importance ratios + log_ratio = token_logp - actor_logps_shifted # [B, T-1] + ratio = torch.exp(log_ratio) # [B, T-1] + + # Select temperature based on advantage sign + # Use where to select between tau_pos and tau_neg without creating new tensors + adv_positive = advantages > 0 # [B] + tau_pos_val = self.tau_pos + tau_neg_val = self.tau_neg + + # Compute soft gate for positive and negative advantages separately + # This allows kernel fusion since we're not creating tensors in the graph + ratio_minus_1 = ratio - 1.0 # [B, T-1] + + # For positive advantages: f(r) = (4/τ_pos) * σ(τ_pos * (r - 1)) + # For negative advantages: f(r) = (4/τ_neg) * σ(τ_neg * (r - 1)) + sigmoid_arg_pos = tau_pos_val * ratio_minus_1 # [B, T-1] + sigmoid_arg_neg = tau_neg_val * ratio_minus_1 # [B, T-1] + + gate_pos = torch.sigmoid(sigmoid_arg_pos) # [B, T-1] + gate_neg = torch.sigmoid(sigmoid_arg_neg) # [B, T-1] + + soft_gate_pos = (4.0 / tau_pos_val) * gate_pos # [B, T-1] + soft_gate_neg = (4.0 / tau_neg_val) * gate_neg # [B, T-1] + + # Select based on advantage sign (broadcast over tokens) + adv_positive_expanded = adv_positive.unsqueeze(-1) # [B, 1] + soft_gate = torch.where(adv_positive_expanded, soft_gate_pos, soft_gate_neg) # [B, T-1] + + # Apply gate to advantages (broadcast advantages over tokens) + adv_expanded = advantages.unsqueeze(-1) # [B, 1] + gated_obj = soft_gate * adv_expanded * token_mask # [B, T-1] + + # Aggregate over tokens + per_sample_obj = gated_obj.sum(dim=-1) # [B] + if self.length_normalize: + per_sample_obj = per_sample_obj / token_counts + + loss = -per_sample_obj.mean() + + # --- Stats computation --- + # Gradient weight: w(r) = 4 * p(r) * (1 - p(r)) + # Select the correct gate based on advantage sign + gate_selected = torch.where(adv_positive_expanded, gate_pos, gate_neg) # [B, T-1] + grad_weight = 4.0 * gate_selected * (1.0 - gate_selected) # [B, T-1] + + # Compute KL for monitoring + token_mismatch_kl = ratio - log_ratio - 1.0 # [B, T-1] + + mask = token_mask > 0 + if mask.any(): + ratio_vals = ratio.masked_select(mask) + ratio_mean = ratio_vals.mean() + ratio_std = ratio_vals.std(unbiased=False) + mismatch_kl = token_mismatch_kl.masked_select(mask).mean() + + # Average gradient weight (for monitoring soft gating) + grad_weight_vals = grad_weight.masked_select(mask) + grad_weight_mean = grad_weight_vals.mean() + grad_weight_std = grad_weight_vals.std(unbiased=False) + else: + ratio_mean = torch.zeros((), device=ratio.device, dtype=ratio.dtype) + ratio_std = torch.zeros((), device=ratio.device, dtype=ratio.dtype) + mismatch_kl = torch.zeros((), device=ratio.device, dtype=ratio.dtype) + grad_weight_mean = torch.zeros((), device=ratio.device, dtype=ratio.dtype) + grad_weight_std = torch.zeros((), device=ratio.device, dtype=ratio.dtype) + + logp_action = (token_logp * token_mask).sum(dim=-1) + stats: Dict[str, Any] = { + "loss": loss.detach(), + "ratio_mean": ratio_mean.detach(), + "ratio_std": ratio_std.detach(), + "grad_weight_mean": grad_weight_mean.detach(), + "grad_weight_std": grad_weight_std.detach(), + "kl_actor_policy": mismatch_kl.detach(), + "adv_mean": advantages.mean().detach(), + "adv_std": advantages.std(unbiased=False).detach(), + "logp_mean": logp_action.mean().detach(), + "avg_action_tokens": token_counts.mean().detach(), + } + return loss, stats + + +@dataclass +class GMPOLoss: + """ + GMPO (Geometric-Mean Policy Optimization) loss. + + GMPO stabilizes GRPO by using the geometric mean of token-level importance ratios + instead of the arithmetic mean. This makes the objective less sensitive to outliers + and results in more stable policy updates. + + Objective: + J_GMPO = E[ (∏_t min(ρ_t * A, clip(ρ_t, 1-ε_low, 1+ε_high) * A))^(1/|o|) * sgn(A) ] + + where: + - ρ_t = π_new(a_t|s_t) / π_old(a_t|s_t) is the token-level importance ratio + - A is the advantage (from batch["weight"]) + - |o| is the sequence length (normalization factor) + - sgn(A) ensures correct optimization direction + + Key differences from GRPO (TokenClippedSurrogateLoss): + 1. Uses geometric mean instead of arithmetic mean (more robust to outliers) + 2. Applies token-level clipping (not sequence-level) + 3. Supports wider clipping ranges (e.g., (e^-0.4, e^0.4) instead of (0.8, 1.2)) + 4. Results in more stable importance sampling ratios during training + + Implementation details: + - All operations performed in log-space for numerical stability + - Clipping applied at token level before geometric mean computation + - Normalization by sequence length (1/|o|) is critical for stability + + Expects: + - batch["weight"]: A (advantages) [B] + - batch["actor_logps"]: token logps under behavior policy [B, T] + - input_ids / attention_mask / action_mask for π_new. + + Reference: "GMPO: Geometric-Mean Policy Optimization" (arXiv:2507.20673v3) + Defaults follow the GMPO paper settings with wider clipping (e^-0.4, e^0.4). + """ + + clip_eps_low: float = 0.4 # In log-space: clip to e^-0.4 ≈ 0.67 + clip_eps_high: float = 0.4 # In log-space: clip to e^0.4 ≈ 1.49 + length_normalize: bool = True # 1/|o| normalization (critical for GMPO) + ratio_clip: Optional[float] = None + + def __post_init__(self) -> None: + if self.clip_eps_low < 0 or self.clip_eps_high < 0: + raise ValueError( + f"clip_eps_low/high must be non-negative, got {self.clip_eps_low}, {self.clip_eps_high}" + ) + if self.ratio_clip is not None and self.ratio_clip <= 0: + raise ValueError(f"ratio_clip must be positive, got {self.ratio_clip}") + + @jaxtyped(typechecker=typechecker) + def compute( + self, + logits: Logits, + batch: Batch, + ) -> Tuple[Tensor, Dict[str, Any]]: + action_mask = batch["action_mask"] + advantages = batch["weight"] # [B] + + if "actor_logps" not in batch: + raise KeyError("GMPOLoss requires batch['actor_logps'] for token IS.") + + shared = _get_shared_context(logits, batch=batch) + if shared is not None: + token_logp = shared.token_logp + token_mask = shared.token_mask + token_counts = shared.token_counts + actor_logps_shifted = shared.actor_logps_shifted + else: + input_ids = batch["input_ids"] + actor_logps = batch["actor_logps"] + if actor_logps.shape != input_ids.shape: + raise ValueError( + f"actor_logps shape {tuple(actor_logps.shape)} does not match input_ids " + f"{tuple(input_ids.shape)}." + ) + + # Compute token-level log probabilities + token_logp = compute_token_logp(logits, input_ids) # [B, T-1] + token_mask = action_mask[:, 1:].to(token_logp.dtype) # [B, T-1] + token_counts = token_mask.sum(dim=-1).clamp(min=1.0) # [B] + actor_logps_shifted = actor_logps[:, 1:] # [B, T-1] + + # Compute log importance ratios (in log-space for numerical stability) + log_ratio = token_logp - actor_logps_shifted # [B, T-1] + + # Sign of advantage (for correct optimization direction) + sgn_adv = torch.sign(advantages).unsqueeze(-1) # [B, 1] + + # Apply advantage sign to log ratios: sgn(A) * log(ρ_t) + sgn_log_ratio = sgn_adv * log_ratio # [B, T-1] + + # Token-level clipping in log-space + # clip(sgn(A) * log(ρ_t), -ε_low, ε_high) + sgn_log_ratio_clipped = torch.clamp( + sgn_log_ratio, + -self.clip_eps_low, + self.clip_eps_high + ) # [B, T-1] + + # Take min of unclipped and clipped (still in log-space, signed) + sgn_log_ratio_min = torch.min(sgn_log_ratio, sgn_log_ratio_clipped) # [B, T-1] + + # Remove sign to get actual log ratios for geometric mean + log_ratio_min = sgn_adv * sgn_log_ratio_min # [B, T-1] + + # Geometric mean: exp(sum(log(ρ_t)) / |o|) = exp(mean(log(ρ_t))) + # Only sum over valid tokens (token_mask == 1) + sum_log_ratio = (log_ratio_min * token_mask).sum(dim=-1) # [B] + + if self.length_normalize: + # Normalize by sequence length: 1/|o| * sum(log(ρ_t)) + geom_mean_log_ratio = sum_log_ratio / token_counts # [B] + else: + geom_mean_log_ratio = sum_log_ratio # [B] + + # Convert back from log-space: ∏_t ρ_t^(1/|o|) + geom_mean_ratio = torch.exp(geom_mean_log_ratio) # [B] + + # Optional ratio clipping (after geometric mean) + if self.ratio_clip is not None: + geom_mean_ratio = torch.clamp(geom_mean_ratio, max=self.ratio_clip) + + # Objective: geom_mean_ratio * A (advantage sign already handled in clipping) + obj = geom_mean_ratio * advantages # [B] + loss = -obj.mean() + + # --- Stats computation --- + # Compute raw ratios for monitoring (not used in loss) + ratio_raw = torch.exp(log_ratio) # [B, T-1] + token_mismatch_kl = ratio_raw - log_ratio - 1.0 # [B, T-1] + + mask = token_mask > 0 + if mask.any(): + ratio_vals = ratio_raw.masked_select(mask) + + # Clip fraction in original ratio space (for comparison with GRPO) + # Note: GMPO clips in log-space, so we convert bounds + lower_bound = torch.exp(torch.tensor(-self.clip_eps_low, device=ratio_vals.device)) + upper_bound = torch.exp(torch.tensor(self.clip_eps_high, device=ratio_vals.device)) + ppo_clip_frac = ( + (ratio_vals > upper_bound) | (ratio_vals < lower_bound) + ).float().mean() + + ratio_mean = ratio_vals.mean() + ratio_std = ratio_vals.std(unbiased=False) + mismatch_kl = token_mismatch_kl.masked_select(mask).mean() + + if self.ratio_clip is not None: + ratio_clip_frac = (geom_mean_ratio >= self.ratio_clip).float().mean() + else: + ratio_clip_frac = torch.zeros((), device=ratio_vals.device, dtype=ratio_vals.dtype) + else: + ratio_mean = torch.zeros((), device=log_ratio.device, dtype=log_ratio.dtype) + ratio_std = torch.zeros((), device=log_ratio.device, dtype=log_ratio.dtype) + ppo_clip_frac = torch.zeros((), device=log_ratio.device, dtype=log_ratio.dtype) + ratio_clip_frac = torch.zeros((), device=log_ratio.device, dtype=log_ratio.dtype) + mismatch_kl = torch.zeros((), device=log_ratio.device, dtype=log_ratio.dtype) + + logp_action = (token_logp * token_mask).sum(dim=-1) + stats: Dict[str, Any] = { + "loss": loss.detach(), + "ratio_mean": ratio_mean.detach(), + "ratio_std": ratio_std.detach(), + "geom_mean_ratio_mean": geom_mean_ratio.mean().detach(), + "geom_mean_ratio_std": geom_mean_ratio.std(unbiased=False).detach(), + "clip_frac": ppo_clip_frac.detach(), + "ratio_clip_frac": ratio_clip_frac.detach(), + "kl_actor_policy": mismatch_kl.detach(), + "adv_mean": advantages.mean().detach(), + "adv_std": advantages.std(unbiased=False).detach(), + "logp_mean": logp_action.mean().detach(), + "avg_action_tokens": token_counts.mean().detach(), + } + return loss, stats + + # --------------------------------------------------------------------------- # KL penalty and entropy bonus # --------------------------------------------------------------------------- +@dataclass +class TokenKLLoss: + """ + Token-level KL penalty between π_new and a reference policy. + + Uses the standard policy-gradient surrogate estimate: + + KL(π_new || π_old) ≈ E_{a ~ π_new} [ log π_new(a|s) - log π_old(a|s) ] + + Computed over action tokens and averaged per sequence if length_normalize=True. + """ + + coeff: float = 1.0 + old_logp_key: str = "actor_logps" + length_normalize: bool = True + + @jaxtyped(typechecker=typechecker) + def compute( + self, + logits: Logits, + batch: Batch, + ) -> Tuple[Tensor, Dict[str, Any]]: + action_mask = batch["action_mask"] + + if self.old_logp_key not in batch: + raise KeyError(f"TokenKLLoss requires batch['{self.old_logp_key}'].") + + shared = _get_shared_context(logits, batch=batch) + if shared is not None: + token_logp = shared.token_logp + token_mask = shared.token_mask + token_counts = shared.token_counts + if self.old_logp_key == "actor_logps": + old_logps_shifted = shared.actor_logps_shifted + else: + input_ids = batch["input_ids"] + old_logps = batch[self.old_logp_key] + if old_logps.shape != input_ids.shape: + raise ValueError( + f"{self.old_logp_key} shape {tuple(old_logps.shape)} does not match input_ids " + f"{tuple(input_ids.shape)}." + ) + old_logps_shifted = old_logps[:, 1:] + else: + input_ids = batch["input_ids"] + old_logps = batch[self.old_logp_key] + if old_logps.shape != input_ids.shape: + raise ValueError( + f"{self.old_logp_key} shape {tuple(old_logps.shape)} does not match input_ids " + f"{tuple(input_ids.shape)}." + ) + token_logp = compute_token_logp(logits, input_ids) + token_mask = action_mask[:, 1:].to(token_logp.dtype) + token_counts = token_mask.sum(dim=-1).clamp(min=1.0) + old_logps_shifted = old_logps[:, 1:] + + token_kl = (token_logp - old_logps_shifted) * token_mask + per_sample_kl = token_kl.sum(dim=-1) + if self.length_normalize: + per_sample_kl = per_sample_kl / token_counts + + loss = self.coeff * per_sample_kl.mean() + + stats: Dict[str, Any] = { + "loss": loss.detach(), + "kl_mean": per_sample_kl.mean().detach(), + "kl_std": per_sample_kl.std(unbiased=False).detach(), + "avg_action_tokens": token_counts.mean().detach(), + } + return loss, stats + + @dataclass class KLLoss: """ @@ -667,22 +1367,32 @@ class KLLoss: length_normalize: bool = False @jaxtyped(typechecker=typechecker) - def compute(self, logits: Logits, batch: Batch) -> Tuple[Tensor, Dict[str, Any]]: - input_ids = batch["input_ids"] + def compute( + self, + logits: Logits, + batch: Batch, + ) -> Tuple[Tensor, Dict[str, Any]]: action_mask = batch["action_mask"] old_logp = batch[self.old_logp_key] # [B] + input_ids = batch["input_ids"] logp_new = compute_logp_action( logits, input_ids, action_mask, length_normalize=self.length_normalize, ) # [B] + shared = _get_shared_context(logits, batch=batch) + if shared is not None: + token_counts = shared.token_counts + else: + token_counts = action_mask[:, 1:].sum(dim=-1).clamp(min=1.0) + if self.length_normalize: - lengths = action_mask[:, 1:].to(old_logp.dtype).sum(dim=-1).clamp(min=1.0) + lengths = token_counts.to(old_logp.dtype) old_logp = old_logp / lengths - kl = logp_new - old_logp # [B] + kl = logp_new - old_logp # [B] loss = self.coeff * kl.mean() stats: Dict[str, Any] = { @@ -709,7 +1419,11 @@ class EntropyBonus: coeff: float = 0.01 @jaxtyped(typechecker=typechecker) - def compute(self, logits: Logits, batch: Batch) -> Tuple[Tensor, Dict[str, Any]]: + def compute( + self, + logits: Logits, + batch: Batch, + ) -> Tuple[Tensor, Dict[str, Any]]: action_mask = batch["action_mask"] logprobs = torch.log_softmax(logits, dim=-1) @@ -720,7 +1434,7 @@ def compute(self, logits: Logits, batch: Batch) -> Tuple[Tensor, Dict[str, Any]] mask = action_mask.to(token_entropy.dtype) - masked_entropy = token_entropy * mask # [B, T] + masked_entropy = token_entropy * mask # [B, T] # avoid divide-by-zero if mask is all zeros denom = mask.sum() if denom.item() == 0: @@ -751,6 +1465,7 @@ class LossTerm: - loss: loss object implementing Loss protocol - weight: scalar multiplier applied to that loss """ + name: str loss: Loss weight: float = 1.0 @@ -768,36 +1483,54 @@ class CompositeLoss: "{name}/loss", "{name}/", ... and a top-level "loss" key for the final combined loss. - - This class expects logits to be passed in, and it passes them - down to all child terms. + + Memory Efficiency: + CompositeLoss automatically creates a SharedContext to cache expensive + intermediate tensors (like token_logp from log_softmax). All child losses + can access the same SharedContext implicitly, sharing cached tensors and + avoiding duplicate autograd graphs. + + Without SharedContext: N losses → N× [B, T, V] autograd activations + With SharedContext: N losses → 1× [B, T, V] autograd activations + + Note: + SharedContext is made available implicitly during computation so + compute_logp_action/compute_token_logp can reuse cached tensors. """ terms: List[LossTerm] @jaxtyped(typechecker=typechecker) - def compute(self, logits: Logits, batch: Batch) -> Tuple[Tensor, Dict[str, Any]]: + def compute( + self, + logits: Logits, + batch: Batch, + ) -> Tuple[Tensor, Dict[str, Any]]: if not self.terms: raise ValueError("CompositeLoss.terms must be non-empty") + # Create shared context for memory-efficient tensor sharing. + shared = SharedContext(logits, batch) + total_loss: Tensor | None = None stats: Dict[str, Any] = {} - for term in self.terms: - # Pass the pre-computed logits down to the child term - raw_loss, term_stats = term.loss.compute(logits, batch) - scaled_loss = term.weight * raw_loss + with _use_shared_context(shared): + for term in self.terms: + raw_loss, term_stats = term.loss.compute(logits, batch) - if total_loss is None: - total_loss = scaled_loss - else: - total_loss = total_loss + scaled_loss + scaled_loss = term.weight * raw_loss + + if total_loss is None: + total_loss = scaled_loss + else: + total_loss = total_loss + scaled_loss - # per-term stats - stats[f"{term.name}/loss"] = raw_loss.detach() - stats[f"{term.name}/weight"] = term.weight - for k, v in term_stats.items(): - stats[f"{term.name}/{k}"] = v + # per-term stats + stats[f"{term.name}/loss"] = raw_loss.detach() + stats[f"{term.name}/weight"] = term.weight + for k, v in term_stats.items(): + stats[f"{term.name}/{k}"] = v assert total_loss is not None stats["loss"] = total_loss.detach() diff --git a/src/ludic/training/trainer.py b/src/ludic/training/trainer.py index 8a01d8a..b312b03 100644 --- a/src/ludic/training/trainer.py +++ b/src/ludic/training/trainer.py @@ -70,8 +70,8 @@ def __init__( model: nn.Module, algo: RLAlgorithm, batch_source: BatchSource, + cfg: TrainerConfig, publisher: Optional[PolicyPublisher] = None, - cfg: TrainerConfig = TrainerConfig(), param_filter: Optional[Callable[[str, Tensor], bool]] = None, enable_gradient_checkpointing: bool = False, checkpointer: Optional[CheckpointManager] = None, @@ -96,14 +96,14 @@ def __init__( The SAWBatch is treated as a macro-batch and split into micro-batches for gradient accumulation. - publisher: - Abstract interface to push weights to inference workers. If None, weight - syncing is disabled. - cfg: TrainerConfig for device, optimizer hyperparams, pad_token_id, micro_token_budget, max_seq_len, and sync_every_steps. + publisher: + Abstract interface to push weights to inference workers. If None, weight + syncing is disabled. + param_filter: Optional predicate (name, Tensor) -> bool deciding which parameters get pushed into the runtime. @@ -575,7 +575,11 @@ async def train_step(self) -> Dict[str, float]: # ---- 2c) Loss + backward (scaled) -------------------------- pre_forward_alloc = self._reset_peak_memory(device) if profile_memory else None try: - loss, stats = self.algo.compute_loss(self.model, batch_tensors) + loss, stats = self.algo.compute_loss( + self.model, + batch_tensors, + cast_logits_to_fp32=self.cfg.cast_logits_to_fp32, + ) # Scale loss by micro-batch size to preserve macro-batch mean. scaled_loss = loss * (item_count / total_items) @@ -697,8 +701,6 @@ def _validate_invariants(self) -> None: raise ValueError( "Trainer evaluation requested (eval_at_start or eval_every_n_steps) but no evaluator was provided." ) - if self.cfg.pad_token_id is None: - raise ValueError("TrainerConfig.pad_token_id must be set for collation.") if self.cfg.max_seq_len < 1: raise ValueError("TrainerConfig.max_seq_len must be >= 1.") if self.cfg.micro_token_budget <= 0: diff --git a/tests/integration/test_code_exec_docker.py b/tests/integration/test_code_exec_docker.py new file mode 100644 index 0000000..ed14ed6 --- /dev/null +++ b/tests/integration/test_code_exec_docker.py @@ -0,0 +1,615 @@ +""" +Integration tests for Docker-based code execution sandbox. + +These tests require Docker to be running and will create/destroy containers. +Run with: pytest -m integration tests/integration/test_code_exec_docker.py + +To skip GPU tests while running integration tests: + pytest -m "integration and not gpu" +""" + +from __future__ import annotations + +import asyncio + +import pytest + +pytestmark = [pytest.mark.integration] + + +# Try to import docker - skip all tests if not available +try: + import docker + from docker.errors import DockerException + + # Try to connect to Docker daemon + try: + _client = docker.from_env() + _client.ping() + _client.close() + DOCKER_AVAILABLE = True + except (DockerException, Exception): + DOCKER_AVAILABLE = False +except ImportError: + DOCKER_AVAILABLE = False + + +skip_if_no_docker = pytest.mark.skipif( + not DOCKER_AVAILABLE, + reason="Docker daemon not available or docker package not installed", +) + + +# --------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------- + + +@pytest.fixture +async def sandbox_pool(): + """Create and tear down a sandbox pool for testing.""" + from ludic.envs.code_exec.docker_sandbox import DockerSandboxPool, DockerSandboxConfig + + config = DockerSandboxConfig( + python_version="3.11", + memory_limit="128m", + cpu_quota=25000, + network_disabled=True, + ) + + pool = DockerSandboxPool( + n_workers=2, + config=config, + cache_size=100, + ) + + await pool.start() + yield pool + await pool.shutdown() + + +@pytest.fixture +async def sandbox(sandbox_pool): + """Get a single sandbox for testing.""" + sandbox = await sandbox_pool.checkout() + yield sandbox + await sandbox_pool.release(sandbox) + + +# --------------------------------------------------------------------- +# DockerSandbox Tests +# --------------------------------------------------------------------- + + +@skip_if_no_docker +class TestDockerSandboxCompile: + @pytest.mark.asyncio + async def test_compile_valid_code(self, sandbox): + """Valid Python code should compile successfully.""" + from ludic.envs.code_exec.types import CompileStatus + + code = """ +def hello(): + return "Hello, World!" + +print(hello()) +""" + result = await sandbox.compile(code) + + assert result.success is True + assert result.status == CompileStatus.SUCCESS + assert result.error_message is None + assert result.duration_ms > 0 + + @pytest.mark.asyncio + async def test_compile_syntax_error(self, sandbox): + """Syntax errors should be detected and reported.""" + from ludic.envs.code_exec.types import CompileStatus + + code = """ +def broken( + print("missing parenthesis") +""" + result = await sandbox.compile(code) + + assert result.success is False + assert result.status == CompileStatus.SYNTAX_ERROR + assert result.error_message is not None + assert "SyntaxError" in result.error_message or "syntax" in result.error_message.lower() + + @pytest.mark.asyncio + async def test_compile_indentation_error(self, sandbox): + """Indentation errors should be detected.""" + from ludic.envs.code_exec.types import CompileStatus + + code = """ +def foo(): +print("bad indent") +""" + result = await sandbox.compile(code) + + assert result.success is False + assert result.status == CompileStatus.SYNTAX_ERROR + + +@skip_if_no_docker +class TestDockerSandboxExecute: + @pytest.mark.asyncio + async def test_execute_simple_print(self, sandbox): + """Simple print statement should produce output.""" + from ludic.envs.code_exec.types import RunStatus + + code = 'print("Hello from Docker!")' + result = await sandbox.execute(code) + + assert result.compiled is True + assert result.succeeded is True + assert result.run_status == RunStatus.SUCCESS + assert "Hello from Docker!" in result.stdout.strip() + assert result.exit_code == 0 + + @pytest.mark.asyncio + async def test_execute_with_stdin(self, sandbox): + """Code should be able to read from stdin.""" + from ludic.envs.code_exec.types import RunStatus + + code = """ +import sys +line = input() +print(f"Got: {line}") +""" + result = await sandbox.execute(code, stdin="test_input") + + assert result.compiled is True + # Note: stdin handling in docker exec is tricky + # This test may need adjustment based on actual behavior + + @pytest.mark.asyncio + async def test_execute_runtime_error(self, sandbox): + """Runtime errors should be captured.""" + from ludic.envs.code_exec.types import RunStatus + + code = """ +x = undefined_variable +""" + result = await sandbox.execute(code) + + assert result.compiled is True + assert result.succeeded is False + assert result.run_status == RunStatus.RUNTIME_ERROR + assert "NameError" in result.stderr or "undefined" in result.stderr.lower() + + @pytest.mark.asyncio + async def test_execute_division_by_zero(self, sandbox): + """Division by zero should be a runtime error.""" + from ludic.envs.code_exec.types import RunStatus + + code = """ +result = 1 / 0 +""" + result = await sandbox.execute(code) + + assert result.compiled is True + assert result.succeeded is False + assert result.run_status == RunStatus.RUNTIME_ERROR + assert "ZeroDivision" in result.stderr + + @pytest.mark.asyncio + async def test_execute_timeout(self, sandbox): + """Infinite loops should timeout.""" + from ludic.envs.code_exec.types import RunStatus + + code = """ +while True: + pass +""" + result = await sandbox.execute(code, timeout_s=1.0) + + assert result.compiled is True + assert result.timed_out is True + assert result.run_status == RunStatus.TIMEOUT + + @pytest.mark.asyncio + async def test_execute_returns_timing(self, sandbox): + """Execution should return timing information.""" + code = """ +import time +time.sleep(0.1) +print("done") +""" + result = await sandbox.execute(code) + + assert result.compile_duration_ms > 0 + assert result.run_duration_ms >= 100 # At least 100ms for sleep + assert result.total_duration_ms > 0 + + +@skip_if_no_docker +class TestDockerSandboxReset: + @pytest.mark.asyncio + async def test_reset_clears_files(self, sandbox): + """Reset should clear workspace files.""" + # Write a file + code1 = """ +with open('test_file.txt', 'w') as f: + f.write('hello') +""" + await sandbox.execute(code1) + + # Reset + await sandbox.reset() + + # Try to read the file - should fail + code2 = """ +try: + with open('test_file.txt', 'r') as f: + print(f.read()) +except FileNotFoundError: + print("FILE_NOT_FOUND") +""" + result = await sandbox.execute(code2) + + assert "FILE_NOT_FOUND" in result.stdout + + +# --------------------------------------------------------------------- +# DockerSandboxPool Tests +# --------------------------------------------------------------------- + + +@skip_if_no_docker +class TestDockerSandboxPool: + @pytest.mark.asyncio + async def test_pool_checkout_and_release(self, sandbox_pool): + """Should be able to checkout and release sandboxes.""" + sandbox = await sandbox_pool.checkout() + assert sandbox is not None + assert sandbox_pool.available == 1 # One still available + + await sandbox_pool.release(sandbox) + assert sandbox_pool.available == 2 # Both available again + + @pytest.mark.asyncio + async def test_pool_concurrent_checkout(self, sandbox_pool): + """Multiple checkouts should work concurrently.""" + sandbox1 = await sandbox_pool.checkout() + sandbox2 = await sandbox_pool.checkout() + + assert sandbox1 is not sandbox2 + assert sandbox_pool.available == 0 + + await sandbox_pool.release(sandbox1) + await sandbox_pool.release(sandbox2) + assert sandbox_pool.available == 2 + + @pytest.mark.asyncio + async def test_pool_checkout_timeout(self, sandbox_pool): + """Checkout should timeout when no sandboxes available.""" + # Check out all sandboxes + sandbox1 = await sandbox_pool.checkout() + sandbox2 = await sandbox_pool.checkout() + + # Third checkout should timeout + with pytest.raises(TimeoutError): + await sandbox_pool.checkout(timeout_s=0.5) + + await sandbox_pool.release(sandbox1) + await sandbox_pool.release(sandbox2) + + @pytest.mark.asyncio + async def test_pool_caching(self, sandbox_pool): + """Pool should cache execution results.""" + from ludic.envs.code_exec.types import ( + BatchTestResult, + CompileResult, + CompileStatus, + ExecutionResult, + RunStatus, + TestCase, + TestResult, + ) + + # Create a mock result + test_result = TestResult( + test_case=TestCase(input="1", expected="2", id="t1"), + passed=True, + actual="2", + execution=ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + ), + ) + batch_result = BatchTestResult( + results=[test_result], + code_hash="abc123", + tests_hash="xyz789", + ) + + # Cache it + sandbox_pool.put_cached("abc123", "xyz789", batch_result) + + # Retrieve it + cached = sandbox_pool.get_cached("abc123", "xyz789") + assert cached is batch_result + + # Check cache stats + stats = sandbox_pool.cache_stats + assert stats["hits"] == 1 + assert stats["size"] == 1 + + +# --------------------------------------------------------------------- +# StdinStdoutRunner Integration Tests +# --------------------------------------------------------------------- + + +@skip_if_no_docker +class TestStdinStdoutRunnerIntegration: + @pytest.mark.asyncio + async def test_runner_all_pass(self, sandbox): + """Runner should correctly execute code and verify outputs.""" + from ludic.envs.code_exec.runners import StdinStdoutRunner + from ludic.envs.code_exec.adapters.base import ExactMatchVerifier + from ludic.envs.code_exec.types import TestCase + + code = """ +n = int(input()) +print(n * 2) +""" + tests = [ + TestCase(input="5", expected="10", id="t1"), + TestCase(input="10", expected="20", id="t2"), + TestCase(input="0", expected="0", id="t3"), + ] + + runner = StdinStdoutRunner(default_timeout_s=5.0) + verifier = ExactMatchVerifier() + + result = await runner.run_tests( + sandbox=sandbox, + code=code, + tests=tests, + verifier=verifier, + ) + + assert result.all_passed is True + assert result.passed_count == 3 + assert result.total_count == 3 + + @pytest.mark.asyncio + async def test_runner_some_fail(self, sandbox): + """Runner should correctly identify failing tests.""" + from ludic.envs.code_exec.runners import StdinStdoutRunner + from ludic.envs.code_exec.adapters.base import ExactMatchVerifier + from ludic.envs.code_exec.types import TestCase + + # Code that only works for positive numbers + code = """ +n = int(input()) +if n < 0: + print("error") +else: + print(n * 2) +""" + tests = [ + TestCase(input="5", expected="10", id="t1"), # Pass + TestCase(input="-5", expected="-10", id="t2"), # Fail + ] + + runner = StdinStdoutRunner(default_timeout_s=5.0) + verifier = ExactMatchVerifier() + + result = await runner.run_tests( + sandbox=sandbox, + code=code, + tests=tests, + verifier=verifier, + stop_on_first_failure=False, + ) + + assert result.all_passed is False + assert result.passed_count == 1 + assert result.total_count == 2 + assert result.results[0].passed is True + assert result.results[1].passed is False + + @pytest.mark.asyncio + async def test_runner_compile_failure(self, sandbox): + """Runner should handle compilation failures gracefully.""" + from ludic.envs.code_exec.runners import StdinStdoutRunner + from ludic.envs.code_exec.adapters.base import ExactMatchVerifier + from ludic.envs.code_exec.types import TestCase + + code = """ +def broken( + print("syntax error") +""" + tests = [ + TestCase(input="1", expected="x", id="t1"), + TestCase(input="2", expected="y", id="t2"), + ] + + runner = StdinStdoutRunner() + verifier = ExactMatchVerifier() + + result = await runner.run_tests( + sandbox=sandbox, + code=code, + tests=tests, + verifier=verifier, + compile_first=True, + ) + + assert result.compile_failed is True + assert result.all_passed is False + assert result.passed_count == 0 + # All tests should be marked as not compiled + for r in result.results: + assert r.compiled is False + + @pytest.mark.asyncio + async def test_runner_stop_on_first_failure(self, sandbox): + """Runner should stop after first failure when configured.""" + from ludic.envs.code_exec.runners import StdinStdoutRunner + from ludic.envs.code_exec.adapters.base import ExactMatchVerifier + from ludic.envs.code_exec.types import TestCase, RunStatus + + code = """ +n = int(input()) +print("wrong" if n == 1 else "correct") +""" + tests = [ + TestCase(input="1", expected="correct", id="t1"), # Fails + TestCase(input="2", expected="correct", id="t2"), # Skipped + TestCase(input="3", expected="correct", id="t3"), # Skipped + ] + + runner = StdinStdoutRunner() + verifier = ExactMatchVerifier() + + result = await runner.run_tests( + sandbox=sandbox, + code=code, + tests=tests, + verifier=verifier, + stop_on_first_failure=True, + ) + + assert result.passed_count == 0 + assert result.results[0].passed is False + assert result.results[0].ran is True + assert result.results[1].ran is False + assert result.results[1].execution.run_status == RunStatus.NOT_RUN + assert result.results[2].ran is False + + +# --------------------------------------------------------------------- +# End-to-End CodeExecEnv Tests +# --------------------------------------------------------------------- + + +@skip_if_no_docker +class TestCodeExecEnvIntegration: + @pytest.mark.asyncio + async def test_env_full_workflow(self, sandbox_pool): + """Test complete workflow from reset to step.""" + from ludic.envs.code_exec.env import CodeExecEnv, CodeExecConfig + from ludic.envs.code_exec.adapters.apps import APPSTestAdapter + + sample = { + "problem_id": "test_add", + "question": "Write a program that reads two integers and prints their sum.", + "inputs": ["1 2", "10 20", "-5 5"], + "outputs": ["3", "30", "0"], + } + + adapter = APPSTestAdapter() + config = CodeExecConfig( + timeout_per_test_s=5.0, + stop_on_first_failure=False, + compile_first=True, + ) + + env = CodeExecEnv( + sample=sample, + sandbox_pool=sandbox_pool, + test_adapter=adapter, + config=config, + ) + + # Reset + obs, info = await env.env_reset() + + assert "two integers" in obs.lower() + assert info["problem_id"] == "test_add" + assert info["num_tests"] == 3 + + # Submit correct code + correct_code = """ +a, b = map(int, input().split()) +print(a + b) +""" + outcome = await env.env_step(correct_code) + + assert outcome.terminated is True + assert outcome.reward == 1.0 + assert outcome.info["all_passed"] is True + assert outcome.info["passed"] == 3 + assert outcome.info["total"] == 3 + + @pytest.mark.asyncio + async def test_env_wrong_code(self, sandbox_pool): + """Test env with incorrect code submission.""" + from ludic.envs.code_exec.env import CodeExecEnv, CodeExecConfig + from ludic.envs.code_exec.adapters.apps import APPSTestAdapter + + sample = { + "problem_id": "test_double", + "question": "Write a program that reads an integer and prints it doubled.", + "inputs": ["5", "10"], + "outputs": ["10", "20"], + } + + adapter = APPSTestAdapter() + config = CodeExecConfig(stop_on_first_failure=False) + + env = CodeExecEnv( + sample=sample, + sandbox_pool=sandbox_pool, + test_adapter=adapter, + config=config, + ) + + await env.env_reset() + + # Submit wrong code (triples instead of doubles) + wrong_code = """ +n = int(input()) +print(n * 3) +""" + outcome = await env.env_step(wrong_code) + + assert outcome.terminated is True + assert outcome.reward == 0.0 # Binary reward, not all passed + assert outcome.info["all_passed"] is False + assert outcome.info["passed"] == 0 + + @pytest.mark.asyncio + async def test_env_partial_credit(self, sandbox_pool): + """Test env with partial credit enabled.""" + from ludic.envs.code_exec.env import CodeExecEnv, CodeExecConfig + from ludic.envs.code_exec.adapters.apps import APPSTestAdapter + + sample = { + "problem_id": "test_abs", + "question": "Write a program that reads an integer and prints its absolute value.", + "inputs": ["5", "-5", "0", "-10"], + "outputs": ["5", "5", "0", "10"], + } + + adapter = APPSTestAdapter() + config = CodeExecConfig( + partial_credit=True, + stop_on_first_failure=False, + ) + + env = CodeExecEnv( + sample=sample, + sandbox_pool=sandbox_pool, + test_adapter=adapter, + config=config, + ) + + await env.env_reset() + + # Code that only works for non-negative numbers + partial_code = """ +n = int(input()) +print(n) # Wrong for negative numbers +""" + outcome = await env.env_step(partial_code) + + assert outcome.terminated is True + assert outcome.info["all_passed"] is False + assert outcome.info["passed"] == 2 # Only positive and zero pass + assert outcome.reward == pytest.approx(0.5) # 2/4 = 0.5 diff --git a/tests/integration/test_grpo_e2e.py b/tests/integration/test_grpo_e2e.py index efe9311..9f31280 100644 --- a/tests/integration/test_grpo_e2e.py +++ b/tests/integration/test_grpo_e2e.py @@ -22,7 +22,7 @@ GroupNormalizedReturn, ) from ludic.interaction.base import InteractionProtocol -from ludic.interaction.single_agent import SingleAgentSyncProtocol +from ludic.interaction.single_agent import SingleAgentProtocol from tests._mocks import SeedableMockAgent @@ -51,7 +51,9 @@ def suggested_sysprompt(self) -> Optional[str]: def env_reset(self, *, seed: Optional[int] = None) -> Tuple[Observation, Info]: self._t = 0 - self._obs = f"Start state for seed {seed}. Correct action is {self.correct_action}." + self._obs = ( + f"Start state for seed {seed}. Correct action is {self.correct_action}." + ) return self._obs, {"seed": seed} def env_step(self, action: str) -> StepOutcome: @@ -110,7 +112,9 @@ def create_protocol() -> InteractionProtocol: return SingleAgentSyncProtocol(agent=agent) protocol_registry = {"grpo_protocol": create_protocol} - engine = RolloutEngine(protocol_registry=protocol_registry, env_registry=env_registry) + engine = RolloutEngine( + protocol_registry=protocol_registry, env_registry=env_registry + ) def make_expanded_requests() -> List[RolloutRequest]: inference = InferenceSpec( diff --git a/tests/test_batch_execution.py b/tests/test_batch_execution.py new file mode 100644 index 0000000..6b70344 --- /dev/null +++ b/tests/test_batch_execution.py @@ -0,0 +1,501 @@ +""" +Unit tests for batch execution functionality. + +Tests the batch execution path in StdinStdoutRunner using mock sandboxes +that implement execute_batch(). +""" + +import pytest +from typing import AsyncIterator, Union + +from ludic.envs.code_exec.runners import StdinStdoutRunner +from ludic.envs.code_exec.types import ( + BatchExecutionSpec, + TestCase, + CompileResult, + CompileStatus, + ExecutionResult, + RunStatus, +) +from ludic.envs.code_exec.adapters.base import ExactMatchVerifier + + +# --------------------------------------------------------------------- +# Mock Sandbox with execute_batch() support +# --------------------------------------------------------------------- + + +class MockBatchSandbox: + """ + A mock sandbox that supports execute_batch() for testing the batched + execution path in StdinStdoutRunner. + + Can be configured with: + - batch_results: List of results to yield from execute_batch() + - compile_success: Whether compilation succeeds + - break_after: If set, raise exception after yielding N results + """ + + def __init__( + self, + batch_results: list[Union[CompileResult, ExecutionResult, dict]] | None = None, + compile_success: bool = True, + break_after: int | None = None, + ): + self._batch_results = batch_results or [] + self._compile_success = compile_success + self._break_after = break_after + self._python_version = "3.11" + + # Track calls + self.execute_batch_calls: list[BatchExecutionSpec] = [] + + @property + def python_version(self) -> str: + return self._python_version + + async def reset(self) -> None: + pass + + async def compile(self, code: str, *, timeout_s: float = 5.0) -> CompileResult: + if self._compile_success: + return CompileResult(status=CompileStatus.SUCCESS, duration_ms=10.0) + return CompileResult( + status=CompileStatus.SYNTAX_ERROR, + error_message="SyntaxError", + duration_ms=5.0, + ) + + async def execute( + self, + code: str, + *, + stdin: str = "", + skip_compile: bool = False, + timeout_s: float = 10.0, + memory_limit_mb: int | None = None, + env_vars: dict[str, str] | None = None, + ) -> ExecutionResult: + # Fallback for non-batch execution + return ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + stdout="", + stderr="", + exit_code=0, + ) + + async def execute_batch( + self, + spec: BatchExecutionSpec, + ) -> AsyncIterator[Union[CompileResult, ExecutionResult, dict]]: + """Yield pre-configured batch results.""" + self.execute_batch_calls.append(spec) + + count = 0 + for result in self._batch_results: + if self._break_after is not None and count >= self._break_after: + raise RuntimeError("Simulated container crash") + yield result + count += 1 + + +def make_success_execution(test_id: str, stdout: str) -> ExecutionResult: + """Helper to create a successful ExecutionResult for a test.""" + return ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + stdout=stdout, + stderr="", + exit_code=0, + cache_key=test_id, # Used to identify which test this result is for + ) + + +def make_failure_execution( + test_id: str, status: RunStatus = RunStatus.RUNTIME_ERROR +) -> ExecutionResult: + """Helper to create a failed ExecutionResult for a test.""" + return ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=status, + stdout="", + stderr="Error occurred", + exit_code=1, + cache_key=test_id, + ) + + +# --------------------------------------------------------------------- +# Batch Execution Tests +# --------------------------------------------------------------------- + + +class TestBatchExecution: + @pytest.mark.asyncio + async def test_batch_all_tests_pass(self): + """All tests pass through batch execution.""" + batch_results = [ + CompileResult(status=CompileStatus.SUCCESS, duration_ms=10.0), + make_success_execution("t1", "expected1"), + make_success_execution("t2", "expected2"), + {"type": "done", "passed": 2, "failed": 0}, + ] + sandbox = MockBatchSandbox(batch_results=batch_results) + runner = StdinStdoutRunner(use_batch_execution=True) + verifier = ExactMatchVerifier() + + tests = [ + TestCase(input="input1", expected="expected1", id="t1"), + TestCase(input="input2", expected="expected2", id="t2"), + ] + + result = await runner.run_tests( + sandbox=sandbox, + code="print('hello')", + tests=tests, + verifier=verifier, + ) + + assert result.all_passed is True + assert result.passed_count == 2 + assert result.total_count == 2 + assert len(sandbox.execute_batch_calls) == 1 + + @pytest.mark.asyncio + async def test_batch_compile_failure(self): + """Compilation failure returns all tests as failed.""" + batch_results = [ + CompileResult( + status=CompileStatus.SYNTAX_ERROR, + error_message="SyntaxError: invalid syntax", + error_line=1, + duration_ms=5.0, + ), + ] + sandbox = MockBatchSandbox(batch_results=batch_results) + runner = StdinStdoutRunner(use_batch_execution=True) + verifier = ExactMatchVerifier() + + tests = [ + TestCase(input="input1", expected="x", id="t1"), + TestCase(input="input2", expected="y", id="t2"), + ] + + result = await runner.run_tests( + sandbox=sandbox, + code="invalid syntax", + tests=tests, + verifier=verifier, + ) + + assert result.compile_failed is True + assert result.all_passed is False + assert result.passed_count == 0 + assert len(result.results) == 2 + + @pytest.mark.asyncio + async def test_batch_some_tests_fail(self): + """Mixed pass/fail through batch execution.""" + batch_results = [ + CompileResult(status=CompileStatus.SUCCESS, duration_ms=10.0), + make_success_execution("t1", "correct"), + make_success_execution("t2", "wrong"), # Output doesn't match expected + {"type": "done", "passed": 1, "failed": 1}, + ] + sandbox = MockBatchSandbox(batch_results=batch_results) + runner = StdinStdoutRunner(use_batch_execution=True) + verifier = ExactMatchVerifier() + + tests = [ + TestCase(input="input1", expected="correct", id="t1"), + TestCase(input="input2", expected="correct", id="t2"), # Will fail + ] + + result = await runner.run_tests( + sandbox=sandbox, + code="code", + tests=tests, + verifier=verifier, + ) + + assert result.all_passed is False + assert result.passed_count == 1 + assert result.total_count == 2 + assert result.results[0].passed is True + assert result.results[1].passed is False + + @pytest.mark.asyncio + async def test_batch_runtime_error(self): + """Runtime error in batch execution.""" + batch_results = [ + CompileResult(status=CompileStatus.SUCCESS, duration_ms=10.0), + make_failure_execution("t1", RunStatus.RUNTIME_ERROR), + {"type": "done", "passed": 0, "failed": 1}, + ] + sandbox = MockBatchSandbox(batch_results=batch_results) + runner = StdinStdoutRunner(use_batch_execution=True) + verifier = ExactMatchVerifier() + + tests = [TestCase(input="input1", expected="output", id="t1")] + + result = await runner.run_tests( + sandbox=sandbox, + code="raise Exception()", + tests=tests, + verifier=verifier, + ) + + assert result.passed_count == 0 + assert result.results[0].passed is False + assert "Runtime error" in (result.results[0].comparison_details or "") + + @pytest.mark.asyncio + async def test_batch_timeout(self): + """Timeout in batch execution.""" + batch_results = [ + CompileResult(status=CompileStatus.SUCCESS, duration_ms=10.0), + make_failure_execution("t1", RunStatus.TIMEOUT), + {"type": "done", "passed": 0, "failed": 1}, + ] + sandbox = MockBatchSandbox(batch_results=batch_results) + runner = StdinStdoutRunner(use_batch_execution=True) + verifier = ExactMatchVerifier() + + tests = [TestCase(input="input1", expected="output", id="t1")] + + result = await runner.run_tests( + sandbox=sandbox, + code="while True: pass", + tests=tests, + verifier=verifier, + ) + + assert result.passed_count == 0 + assert result.results[0].passed is False + + @pytest.mark.asyncio + async def test_batch_stop_on_first_failure_spec(self): + """Verify stop_on_first_failure is passed to BatchExecutionSpec.""" + batch_results = [ + CompileResult(status=CompileStatus.SUCCESS, duration_ms=10.0), + make_success_execution("t1", "output"), + {"type": "done", "passed": 1, "failed": 0}, + ] + sandbox = MockBatchSandbox(batch_results=batch_results) + runner = StdinStdoutRunner(use_batch_execution=True) + verifier = ExactMatchVerifier() + + tests = [TestCase(input="input1", expected="output", id="t1")] + + await runner.run_tests( + sandbox=sandbox, + code="code", + tests=tests, + verifier=verifier, + stop_on_first_failure=True, + ) + + assert len(sandbox.execute_batch_calls) == 1 + spec = sandbox.execute_batch_calls[0] + assert spec.stop_on_first_failure is True + + @pytest.mark.asyncio + async def test_batch_broken_stream_sandbox_error(self): + """Broken stream marks missing tests as SANDBOX_ERROR.""" + # Stream breaks after compile result, before any test results + batch_results = [ + CompileResult(status=CompileStatus.SUCCESS, duration_ms=10.0), + make_success_execution("t1", "output1"), + # Stream breaks here - t2 and t3 never received + ] + sandbox = MockBatchSandbox(batch_results=batch_results, break_after=2) + runner = StdinStdoutRunner(use_batch_execution=True) + verifier = ExactMatchVerifier() + + tests = [ + TestCase(input="input1", expected="output1", id="t1"), + TestCase(input="input2", expected="output2", id="t2"), + TestCase(input="input3", expected="output3", id="t3"), + ] + + result = await runner.run_tests( + sandbox=sandbox, + code="code", + tests=tests, + verifier=verifier, + ) + + # t1 should have succeeded, t2 and t3 should be SANDBOX_ERROR + assert len(result.results) == 3 + assert result.results[0].passed is True + assert result.results[0].test_case.id == "t1" + + # Find t2 and t3 results (order may vary due to dict iteration) + t2_result = next(r for r in result.results if r.test_case.id == "t2") + t3_result = next(r for r in result.results if r.test_case.id == "t3") + + assert t2_result.passed is False + assert t2_result.execution.run_status == RunStatus.SANDBOX_ERROR + assert "Sandbox crashed" in (t2_result.comparison_details or "") + + assert t3_result.passed is False + assert t3_result.execution.run_status == RunStatus.SANDBOX_ERROR + + @pytest.mark.asyncio + async def test_batch_no_done_marker_adds_missing(self): + """Missing 'done' marker triggers fallback for unreceived tests.""" + # No "done" marker, but some tests received + batch_results = [ + CompileResult(status=CompileStatus.SUCCESS, duration_ms=10.0), + make_success_execution("t1", "output1"), + # No "done" marker - stream ended unexpectedly + ] + sandbox = MockBatchSandbox(batch_results=batch_results) + runner = StdinStdoutRunner(use_batch_execution=True) + verifier = ExactMatchVerifier() + + tests = [ + TestCase(input="input1", expected="output1", id="t1"), + TestCase(input="input2", expected="output2", id="t2"), + ] + + result = await runner.run_tests( + sandbox=sandbox, + code="code", + tests=tests, + verifier=verifier, + ) + + assert len(result.results) == 2 + assert result.results[0].passed is True + + # t2 should be marked as SANDBOX_ERROR + t2_result = next(r for r in result.results if r.test_case.id == "t2") + assert t2_result.execution.run_status == RunStatus.SANDBOX_ERROR + + @pytest.mark.asyncio + async def test_batch_disabled_falls_back_to_individual(self): + """With use_batch_execution=False, individual execution is used.""" + sandbox = MockBatchSandbox() + runner = StdinStdoutRunner(use_batch_execution=False) + verifier = ExactMatchVerifier() + + tests = [TestCase(input="input1", expected="", id="t1")] + + await runner.run_tests( + sandbox=sandbox, + code="code", + tests=tests, + verifier=verifier, + ) + + # execute_batch should NOT be called + assert len(sandbox.execute_batch_calls) == 0 + + @pytest.mark.asyncio + async def test_batch_spec_contains_all_test_info(self): + """Verify BatchExecutionSpec contains all test information.""" + batch_results = [ + CompileResult(status=CompileStatus.SUCCESS), + make_success_execution("t1", "out"), + {"type": "done"}, + ] + sandbox = MockBatchSandbox(batch_results=batch_results) + runner = StdinStdoutRunner(use_batch_execution=True, default_timeout_s=7.5) + verifier = ExactMatchVerifier() + + tests = [TestCase(input="my_input", expected="out", id="t1")] + + await runner.run_tests( + sandbox=sandbox, + code="my_code", + tests=tests, + verifier=verifier, + compile_first=True, + stop_on_first_failure=False, + ) + + assert len(sandbox.execute_batch_calls) == 1 + spec = sandbox.execute_batch_calls[0] + + assert spec.code == "my_code" + assert len(spec.tests) == 1 + assert spec.tests[0].id == "t1" + assert spec.tests[0].input == "my_input" + assert spec.compile_first is True + assert spec.stop_on_first_failure is False + assert spec.timeout_s == 7.5 + + @pytest.mark.asyncio + async def test_batch_hashes_computed(self): + """Verify code_hash and tests_hash are computed for batch execution.""" + batch_results = [ + CompileResult(status=CompileStatus.SUCCESS), + make_success_execution("t1", "output"), + {"type": "done"}, + ] + sandbox = MockBatchSandbox(batch_results=batch_results) + runner = StdinStdoutRunner(use_batch_execution=True) + verifier = ExactMatchVerifier() + + code = "print('hello')" + tests = [TestCase(input="input1", expected="output", id="t1")] + + result = await runner.run_tests( + sandbox=sandbox, + code=code, + tests=tests, + verifier=verifier, + ) + + # Verify hashes are present + assert len(result.code_hash) == 16 + assert len(result.tests_hash) == 16 + assert all(c in "0123456789abcdef" for c in result.code_hash) + + +class TestBatchExecutionNotRunStatus: + """Tests for NOT_RUN status handling in batch execution.""" + + @pytest.mark.asyncio + async def test_not_run_tests_from_batch_stream(self): + """Tests marked as NOT_RUN in batch stream are handled correctly.""" + batch_results = [ + CompileResult(status=CompileStatus.SUCCESS, duration_ms=10.0), + make_failure_execution("t1", RunStatus.RUNTIME_ERROR), + # t2 marked as not_run by batch_runner due to stop_on_first_failure + ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.NOT_RUN, + stdout="", + stderr="", + exit_code=None, + cache_key="t2", + ), + {"type": "done", "passed": 0, "failed": 1}, + ] + sandbox = MockBatchSandbox(batch_results=batch_results) + runner = StdinStdoutRunner(use_batch_execution=True) + verifier = ExactMatchVerifier() + + tests = [ + TestCase(input="input1", expected="output1", id="t1"), + TestCase(input="input2", expected="output2", id="t2"), + ] + + result = await runner.run_tests( + sandbox=sandbox, + code="code", + tests=tests, + verifier=verifier, + stop_on_first_failure=True, + ) + + assert len(result.results) == 2 + assert result.results[0].passed is False + assert result.results[0].execution.run_status == RunStatus.RUNTIME_ERROR + + t2_result = next(r for r in result.results if r.test_case.id == "t2") + assert t2_result.passed is False + assert t2_result.execution.run_status == RunStatus.NOT_RUN diff --git a/tests/test_code_exec_adapters.py b/tests/test_code_exec_adapters.py new file mode 100644 index 0000000..2d701bd --- /dev/null +++ b/tests/test_code_exec_adapters.py @@ -0,0 +1,335 @@ +""" +Unit tests for ludic.envs.code_exec.adapters + +Tests verifiers and test adapters. +""" + +import pytest + +from ludic.envs.code_exec.adapters.base import ( + ExactMatchVerifier, + WhitespaceNormalizedVerifier, + FloatTolerantVerifier, +) +from ludic.envs.code_exec.adapters.apps import ( + APPSTestAdapter, + APPS_SYSTEM_PROMPT, +) +from ludic.envs.code_exec.types import TestCase + + +# --------------------------------------------------------------------- +# ExactMatchVerifier Tests +# --------------------------------------------------------------------- + + +class TestExactMatchVerifier: + def test_exact_match_passes(self): + verifier = ExactMatchVerifier() + passed, details = verifier.verify("hello", "hello") + assert passed is True + assert details is None + + def test_mismatch_fails(self): + verifier = ExactMatchVerifier() + passed, details = verifier.verify("hello", "world") + assert passed is False + assert details is not None + + def test_strips_whitespace_by_default(self): + verifier = ExactMatchVerifier() + passed, _ = verifier.verify(" hello \n", "hello") + assert passed is True + + def test_strip_disabled(self): + verifier = ExactMatchVerifier(strip=False) + passed, _ = verifier.verify("hello ", "hello") + assert passed is False + + def test_case_sensitive_by_default(self): + verifier = ExactMatchVerifier() + passed, _ = verifier.verify("Hello", "hello") + assert passed is False + + def test_case_insensitive(self): + verifier = ExactMatchVerifier(case_sensitive=False) + passed, _ = verifier.verify("HELLO", "hello") + assert passed is True + + def test_length_mismatch_details(self): + verifier = ExactMatchVerifier() + passed, details = verifier.verify("abc", "abcdef") + assert passed is False + assert "Length mismatch" in details + assert "3" in details + assert "6" in details + + def test_first_diff_details(self): + verifier = ExactMatchVerifier() + passed, details = verifier.verify("abc", "axc") + assert passed is False + assert "First diff" in details + + +# --------------------------------------------------------------------- +# WhitespaceNormalizedVerifier Tests +# --------------------------------------------------------------------- + + +class TestWhitespaceNormalizedVerifier: + def test_normalizes_multiple_spaces(self): + verifier = WhitespaceNormalizedVerifier() + passed, _ = verifier.verify("hello world", "hello world") + assert passed is True + + def test_normalizes_newlines(self): + verifier = WhitespaceNormalizedVerifier() + passed, _ = verifier.verify("hello\n\nworld", "hello world") + assert passed is True + + def test_normalizes_tabs(self): + verifier = WhitespaceNormalizedVerifier() + passed, _ = verifier.verify("hello\t\tworld", "hello world") + assert passed is True + + def test_normalizes_mixed_whitespace(self): + verifier = WhitespaceNormalizedVerifier() + passed, _ = verifier.verify(" hello \n\t world ", "hello world") + assert passed is True + + def test_content_mismatch_fails(self): + verifier = WhitespaceNormalizedVerifier() + passed, _ = verifier.verify("hello world", "hello mars") + assert passed is False + + +# --------------------------------------------------------------------- +# FloatTolerantVerifier Tests +# --------------------------------------------------------------------- + + +class TestFloatTolerantVerifier: + def test_exact_float_match(self): + verifier = FloatTolerantVerifier() + passed, _ = verifier.verify("3.14159", "3.14159") + assert passed is True + + def test_float_within_tolerance(self): + verifier = FloatTolerantVerifier(abs_tol=1e-6) + passed, _ = verifier.verify("3.141590001", "3.14159") + assert passed is True + + def test_float_outside_tolerance(self): + verifier = FloatTolerantVerifier(abs_tol=1e-9) + passed, _ = verifier.verify("3.15", "3.14") + assert passed is False + + def test_integer_match(self): + verifier = FloatTolerantVerifier() + passed, _ = verifier.verify("42", "42") + assert passed is True + + def test_string_exact_match(self): + verifier = FloatTolerantVerifier() + passed, _ = verifier.verify("hello", "hello") + assert passed is True + + def test_string_mismatch(self): + verifier = FloatTolerantVerifier() + passed, _ = verifier.verify("hello", "world") + assert passed is False + + def test_multiple_tokens(self): + verifier = FloatTolerantVerifier(abs_tol=1e-6) + passed, _ = verifier.verify("1.0 2.0 3.0", "1.0 2.0 3.0") + assert passed is True + + def test_multiple_tokens_within_tolerance(self): + verifier = FloatTolerantVerifier(abs_tol=0.01) + passed, _ = verifier.verify("1.001 2.002 3.003", "1.0 2.0 3.0") + assert passed is True + + def test_token_count_mismatch(self): + verifier = FloatTolerantVerifier() + passed, details = verifier.verify("1 2", "1 2 3") + assert passed is False + assert "Token count mismatch" in details + + def test_relative_tolerance(self): + verifier = FloatTolerantVerifier(rel_tol=0.01, abs_tol=0) + # 1% of 100 = 1, so 100.5 should match 100 + passed, _ = verifier.verify("100.5", "100") + assert passed is True + + def test_strips_whitespace(self): + verifier = FloatTolerantVerifier() + passed, _ = verifier.verify(" 42 ", "42") + assert passed is True + + +# --------------------------------------------------------------------- +# APPSTestAdapter Tests +# --------------------------------------------------------------------- + + +class TestAPPSTestAdapter: + def test_get_prompt_extracts_question(self): + adapter = APPSTestAdapter() + sample = { + "question": "Write a function to add two numbers.", + "inputs": ["1 2"], + "outputs": ["3"], + } + prompt = adapter.get_prompt(sample) + assert prompt == "Write a function to add two numbers." + + def test_get_prompt_with_custom_key(self): + adapter = APPSTestAdapter(question_key="problem_description") + sample = { + "problem_description": "Custom problem text", + "inputs": [], + "outputs": [], + } + prompt = adapter.get_prompt(sample) + assert prompt == "Custom problem text" + + def test_get_problem_id(self): + adapter = APPSTestAdapter() + sample = { + "problem_id": "prob_123", + "question": "Q", + "inputs": [], + "outputs": [], + } + assert adapter.get_problem_id(sample) == "prob_123" + + def test_get_problem_id_missing_returns_unknown(self): + adapter = APPSTestAdapter() + sample = { + "question": "Q", + "inputs": [], + "outputs": [], + } + assert adapter.get_problem_id(sample) == "unknown" + + def test_get_problem_id_custom_key(self): + adapter = APPSTestAdapter(problem_id_key="id") + sample = { + "id": "custom_id", + "question": "Q", + "inputs": [], + "outputs": [], + } + assert adapter.get_problem_id(sample) == "custom_id" + + def test_get_tests_single_test(self): + adapter = APPSTestAdapter() + sample = { + "question": "Q", + "inputs": ["1 2"], + "outputs": ["3"], + } + tests = adapter.get_tests(sample) + assert len(tests) == 1 + assert tests[0].input == "1 2" + assert tests[0].expected == "3" + assert tests[0].id == "test_0" + + def test_get_tests_multiple_tests(self): + adapter = APPSTestAdapter() + sample = { + "question": "Q", + "inputs": ["1", "2", "3"], + "outputs": ["a", "b", "c"], + } + tests = adapter.get_tests(sample) + assert len(tests) == 3 + assert tests[0].input == "1" + assert tests[0].expected == "a" + assert tests[0].id == "test_0" + assert tests[1].input == "2" + assert tests[1].expected == "b" + assert tests[1].id == "test_1" + assert tests[2].input == "3" + assert tests[2].expected == "c" + assert tests[2].id == "test_2" + + def test_get_tests_mismatched_length_raises(self): + adapter = APPSTestAdapter() + sample = { + "question": "Q", + "inputs": ["1", "2", "3"], + "outputs": ["a", "b"], # One less + } + with pytest.raises(ValueError) as exc_info: + adapter.get_tests(sample) + assert "Mismatched" in str(exc_info.value) + + def test_get_tests_custom_keys(self): + adapter = APPSTestAdapter(inputs_key="test_inputs", outputs_key="test_outputs") + sample = { + "question": "Q", + "test_inputs": ["x"], + "test_outputs": ["y"], + } + tests = adapter.get_tests(sample) + assert len(tests) == 1 + assert tests[0].input == "x" + assert tests[0].expected == "y" + + def test_hash_tests_deterministic(self): + adapter = APPSTestAdapter() + tests = [ + TestCase(input="1", expected="a", id="t1"), + TestCase(input="2", expected="b", id="t2"), + ] + hash1 = adapter.hash_tests(tests) + hash2 = adapter.hash_tests(tests) + assert hash1 == hash2 + assert len(hash1) == 16 # 16 hex chars + + def test_hash_tests_different_for_different_tests(self): + adapter = APPSTestAdapter() + tests1 = [TestCase(input="1", expected="a", id="t1")] + tests2 = [TestCase(input="2", expected="b", id="t1")] + hash1 = adapter.hash_tests(tests1) + hash2 = adapter.hash_tests(tests2) + assert hash1 != hash2 + + def test_hash_tests_order_matters(self): + adapter = APPSTestAdapter() + tests1 = [ + TestCase(input="1", expected="a", id="t1"), + TestCase(input="2", expected="b", id="t2"), + ] + tests2 = [ + TestCase(input="2", expected="b", id="t2"), + TestCase(input="1", expected="a", id="t1"), + ] + hash1 = adapter.hash_tests(tests1) + hash2 = adapter.hash_tests(tests2) + assert hash1 != hash2 + + def test_hash_tests_ignores_id(self): + """Hash should be based on input/expected, not id.""" + adapter = APPSTestAdapter() + tests1 = [TestCase(input="1", expected="a", id="test_0")] + tests2 = [TestCase(input="1", expected="a", id="different_id")] + hash1 = adapter.hash_tests(tests1) + hash2 = adapter.hash_tests(tests2) + assert hash1 == hash2 + + +class TestAPPSSystemPrompt: + def test_system_prompt_exists(self): + assert APPS_SYSTEM_PROMPT is not None + assert len(APPS_SYSTEM_PROMPT) > 0 + + def test_system_prompt_mentions_python(self): + assert "Python" in APPS_SYSTEM_PROMPT or "python" in APPS_SYSTEM_PROMPT + + def test_system_prompt_mentions_stdin(self): + assert "stdin" in APPS_SYSTEM_PROMPT + + def test_system_prompt_mentions_stdout(self): + assert "stdout" in APPS_SYSTEM_PROMPT diff --git a/tests/test_code_exec_async_protocol.py b/tests/test_code_exec_async_protocol.py new file mode 100644 index 0000000..c45142a --- /dev/null +++ b/tests/test_code_exec_async_protocol.py @@ -0,0 +1,465 @@ +""" +Integration tests for async env support in SingleAgentProtocol. + +Tests that the protocol correctly detects and handles envs with async +env_reset and env_step methods (like CodeExecEnv). +""" + +from typing import Optional, Tuple + +import pytest + +from ludic.context.full_dialog import FullDialog +from ludic.interaction.single_agent import SingleAgentProtocol, _has_async_env_methods +from ludic.agents.base_agent import Agent +from ludic.envs.single_agent_env import SingleAgentEnv +from ludic.parsers import ParseResult +from ludic.types import Info, Observation, StepOutcome +from tests._mocks import MockClient + + +# Simple pass-through parser for tests +def _passthrough_parser(raw: str) -> ParseResult: + return ParseResult(action=raw, reward=0.0, obs=None) + + +# --------------------------------------------------------------------- +# Mock Async Env for Testing +# --------------------------------------------------------------------- + + +class MockAsyncEnv(SingleAgentEnv): + """ + A mock async env that simulates CodeExecEnv behavior. + + Has async env_reset and env_step methods, unlike standard sync envs. + """ + + def __init__( + self, + target_action: str = "correct_code", + max_steps: int = 3, + ): + super().__init__() + self._target_action = target_action + self._max_steps = max_steps + self._step_count = 0 + self._obs = "Write code to solve the problem." + + # Track calls for assertions + self.reset_calls = 0 + self.step_calls = 0 + + @property + def suggested_sysprompt(self) -> Optional[str]: + return "You are a code assistant." + + async def env_reset(self, *, seed: Optional[int] = None) -> Tuple[Observation, Info]: + """Async reset method (like CodeExecEnv).""" + self.reset_calls += 1 + self._step_count = 0 + self._obs = "Write code to solve the problem." + return self._obs, {"problem_id": "test_problem", "async_env": True} + + async def env_step(self, action: str) -> StepOutcome: + """Async step method (like CodeExecEnv).""" + self.step_calls += 1 + self._step_count += 1 + + if action == self._target_action: + # Correct code - terminate with success + return StepOutcome( + obs="All tests passed!", + reward=1.0, + truncated=False, + terminated=True, + info={"all_passed": True, "step_count": self._step_count}, + ) + elif self._step_count >= self._max_steps: + # Max steps reached - truncate + return StepOutcome( + obs=f"Tests failed. Attempt {self._step_count}/{self._max_steps}.", + reward=-0.1, + truncated=True, + terminated=False, + info={"all_passed": False, "step_count": self._step_count}, + ) + else: + # Wrong code but more attempts allowed + return StepOutcome( + obs=f"Tests failed. Try again. Attempt {self._step_count}/{self._max_steps}.", + reward=-0.1, + truncated=False, + terminated=False, + info={"all_passed": False, "step_count": self._step_count}, + ) + + def env_current_obs(self) -> Observation: + return self._obs + + +class MockSyncEnv(SingleAgentEnv): + """ + A standard sync env for comparison testing. + Uses regular (non-async) env_reset and env_step. + """ + + def __init__(self, target_action: str = "correct"): + super().__init__() + self._target_action = target_action + self._obs = "Sync env observation" + self.reset_calls = 0 + self.step_calls = 0 + + def env_reset(self, *, seed: Optional[int] = None) -> Tuple[Observation, Info]: + """Standard sync reset.""" + self.reset_calls += 1 + self._obs = "Sync env observation" + return self._obs, {"sync_env": True} + + def env_step(self, action: str) -> StepOutcome: + """Standard sync step.""" + self.step_calls += 1 + terminated = action == self._target_action + return StepOutcome( + obs="Success" if terminated else "Wrong", + reward=1.0 if terminated else -0.1, + truncated=False, + terminated=terminated, + info={}, + ) + + def env_current_obs(self) -> Observation: + return self._obs + + +# --------------------------------------------------------------------- +# Async Detection Tests +# --------------------------------------------------------------------- + + +class TestAsyncDetection: + def test_detects_async_reset(self): + env = MockAsyncEnv() + has_async_reset, has_async_step = _has_async_env_methods(env) + assert has_async_reset is True + + def test_detects_async_step(self): + env = MockAsyncEnv() + has_async_reset, has_async_step = _has_async_env_methods(env) + assert has_async_step is True + + def test_detects_sync_env(self): + env = MockSyncEnv() + has_async_reset, has_async_step = _has_async_env_methods(env) + assert has_async_reset is False + assert has_async_step is False + + +# --------------------------------------------------------------------- +# Protocol Async Env Integration Tests +# --------------------------------------------------------------------- + + +class TestProtocolAsyncEnvIntegration: + @pytest.mark.asyncio + async def test_protocol_runs_async_env_successfully(self): + """Protocol should correctly run an async env and produce rollouts.""" + env = MockAsyncEnv(target_action="correct_code") + agent = Agent( + client=MockClient(text="correct_code"), + model="mock", + ctx=FullDialog(), + parser=_passthrough_parser, + ) + protocol = SingleAgentProtocol(agent=agent) + + rollouts = await protocol.run(env=env, max_steps=5) + + assert len(rollouts) == 1 + rollout = rollouts[0] + + # Should terminate on first step with correct action + assert len(rollout.steps) == 1 + assert rollout.steps[0].terminated is True + assert rollout.steps[0].reward == pytest.approx(1.0) + assert rollout.steps[0].info.get("all_passed") is True + + @pytest.mark.asyncio + async def test_protocol_calls_async_reset(self): + """Protocol should call async env_reset and receive correct observation.""" + env = MockAsyncEnv() + agent = Agent( + client=MockClient(text="wrong_code"), + model="mock", + ctx=FullDialog(), + parser=_passthrough_parser, + ) + protocol = SingleAgentProtocol(agent=agent) + + await protocol.run(env=env, max_steps=1) + + # Verify async reset was called + assert env.reset_calls == 1 + + @pytest.mark.asyncio + async def test_protocol_calls_async_step(self): + """Protocol should call async env_step with the parsed action.""" + env = MockAsyncEnv() + agent = Agent( + client=MockClient(text="some_code"), + model="mock", + ctx=FullDialog(), + parser=_passthrough_parser, + ) + protocol = SingleAgentProtocol(agent=agent) + + await protocol.run(env=env, max_steps=1) + + # Verify async step was called + assert env.step_calls == 1 + + @pytest.mark.asyncio + async def test_protocol_uses_async_env_system_prompt(self): + """Protocol should use the async env's suggested_sysprompt.""" + env = MockAsyncEnv() + agent = Agent( + client=MockClient(text="code"), + model="mock", + ctx=FullDialog(), + parser=_passthrough_parser, + ) + protocol = SingleAgentProtocol(agent=agent) + + await protocol.run(env=env, max_steps=1) + + # The agent context should have the system prompt from env + messages = agent._ctx.messages + # First message should be system prompt + assert any( + m.get("role") == "system" and "code assistant" in m.get("content", "").lower() + for m in messages + ) + + @pytest.mark.asyncio + async def test_async_env_multiple_steps(self): + """Test that async env works correctly over multiple steps.""" + env = MockAsyncEnv(target_action="correct", max_steps=5) + + # Agent says "wrong" first 2 times, then "correct" + call_count = 0 + + class CountingClient(MockClient): + def __init__(self): + super().__init__(text="wrong") + + async def complete(self, *args, **kwargs): + nonlocal call_count + call_count += 1 + if call_count < 3: + self._text = "wrong" + else: + self._text = "correct" + return await super().complete(*args, **kwargs) + + agent = Agent( + client=CountingClient(), + model="mock", + ctx=FullDialog(), + parser=_passthrough_parser, + ) + protocol = SingleAgentProtocol(agent=agent) + + rollouts = await protocol.run(env=env, max_steps=10) + + assert len(rollouts) == 1 + rollout = rollouts[0] + + # Should have taken 3 steps to get correct answer + assert len(rollout.steps) == 3 + assert rollout.steps[0].terminated is False + assert rollout.steps[1].terminated is False + assert rollout.steps[2].terminated is True + + # Total reward: -0.1 + -0.1 + 1.0 = 0.8 + assert rollout.total_reward == pytest.approx(0.8) + + +# --------------------------------------------------------------------- +# Backward Compatibility Tests +# --------------------------------------------------------------------- + + +class TestBackwardCompatibility: + @pytest.mark.asyncio + async def test_sync_env_still_works(self): + """Sync envs should continue to work without changes.""" + env = MockSyncEnv(target_action="correct") + agent = Agent( + client=MockClient(text="correct"), + model="mock", + ctx=FullDialog(), + parser=_passthrough_parser, + ) + protocol = SingleAgentProtocol(agent=agent) + + rollouts = await protocol.run(env=env, max_steps=5) + + assert len(rollouts) == 1 + rollout = rollouts[0] + + assert len(rollout.steps) == 1 + assert rollout.steps[0].terminated is True + assert rollout.steps[0].reward == pytest.approx(1.0) + + @pytest.mark.asyncio + async def test_sync_env_reset_is_called(self): + """Sync env reset should be called through normal path.""" + env = MockSyncEnv() + agent = Agent( + client=MockClient(text="wrong"), + model="mock", + ctx=FullDialog(), + parser=_passthrough_parser, + ) + protocol = SingleAgentProtocol(agent=agent) + + await protocol.run(env=env, max_steps=1) + + assert env.reset_calls == 1 + + @pytest.mark.asyncio + async def test_sync_env_step_is_called(self): + """Sync env step should be called through normal path.""" + env = MockSyncEnv() + agent = Agent( + client=MockClient(text="wrong"), + model="mock", + ctx=FullDialog(), + parser=_passthrough_parser, + ) + protocol = SingleAgentProtocol(agent=agent) + + await protocol.run(env=env, max_steps=1) + + assert env.step_calls == 1 + + +# --------------------------------------------------------------------- +# Info Propagation Tests +# --------------------------------------------------------------------- + + +class TestAsyncEnvInfoPropagation: + @pytest.mark.asyncio + async def test_reset_info_accessible_in_rollout(self): + """Info from async env_reset should be accessible.""" + env = MockAsyncEnv() + agent = Agent( + client=MockClient(text="correct_code"), + model="mock", + ctx=FullDialog(), + parser=_passthrough_parser, + ) + protocol = SingleAgentProtocol(agent=agent) + + rollouts = await protocol.run(env=env, max_steps=1) + + # The first step's prev_obs should be from reset + assert rollouts[0].steps[0].prev_obs == "Write code to solve the problem." + + @pytest.mark.asyncio + async def test_step_info_propagated_to_rollout(self): + """Info from async env_step should be in the step info.""" + env = MockAsyncEnv() + agent = Agent( + client=MockClient(text="correct_code"), + model="mock", + ctx=FullDialog(), + parser=_passthrough_parser, + ) + protocol = SingleAgentProtocol(agent=agent) + + rollouts = await protocol.run(env=env, max_steps=1) + + step_info = rollouts[0].steps[0].info + assert step_info.get("all_passed") is True + assert step_info.get("step_count") == 1 + + +# --------------------------------------------------------------------- +# Edge Cases +# --------------------------------------------------------------------- + + +class TestAsyncEnvEdgeCases: + @pytest.mark.asyncio + async def test_async_env_truncation_on_max_steps(self): + """Async env that never terminates should truncate at max_steps.""" + env = MockAsyncEnv(target_action="impossible", max_steps=100) + agent = Agent( + client=MockClient(text="wrong"), + model="mock", + ctx=FullDialog(), + parser=_passthrough_parser, + ) + protocol = SingleAgentProtocol(agent=agent) + + rollouts = await protocol.run(env=env, max_steps=3) + + rollout = rollouts[0] + assert len(rollout.steps) == 3 + assert rollout.steps[-1].truncated is True + assert rollout.meta.get("episode_truncated") is True + + @pytest.mark.asyncio + async def test_async_env_with_parser(self): + """Parser should work correctly with async envs.""" + from ludic.parsers import xml_tag_parser + + env = MockAsyncEnv(target_action="parsed_code") + agent = Agent( + client=MockClient(text="parsed_code"), + model="mock", + ctx=FullDialog(), + parser=xml_tag_parser("code"), + ) + protocol = SingleAgentProtocol(agent=agent) + + rollouts = await protocol.run(env=env, max_steps=5) + + rollout = rollouts[0] + assert len(rollout.steps) == 1 + assert rollout.steps[0].terminated is True + assert rollout.steps[0].info.get("parsed_action") == "parsed_code" + + @pytest.mark.asyncio + async def test_async_env_parser_failure(self): + """Parser failures should be handled correctly with async envs.""" + from ludic.parsers import ParseResult + + def strict_parser(text: str) -> ParseResult: + if text.startswith("VALID:"): + return ParseResult(action=text[6:], reward=0.1, obs=None) + return ParseResult(action=None, reward=-0.5, obs="Invalid format") + + env = MockAsyncEnv() + agent = Agent( + client=MockClient(text="invalid_format"), + model="mock", + ctx=FullDialog(), + parser=strict_parser, + ) + protocol = SingleAgentProtocol(agent=agent) + + rollouts = await protocol.run(env=env, max_steps=1) + + rollout = rollouts[0] + assert len(rollout.steps) == 1 + step = rollout.steps[0] + + # Parser failure - no env step called + assert env.step_calls == 0 + assert step.info.get("parse_error") is True + assert step.reward == pytest.approx(-0.5) + assert step.next_obs == "Invalid format" diff --git a/tests/test_code_exec_cache.py b/tests/test_code_exec_cache.py new file mode 100644 index 0000000..e04d929 --- /dev/null +++ b/tests/test_code_exec_cache.py @@ -0,0 +1,392 @@ +""" +Unit tests for ludic.envs.code_exec.docker_sandbox.LRUCache + +Tests thread safety, eviction behavior, and statistics tracking. + +Note: Requires the `docker` package to be installed for LRUCache import. +""" + +import threading +import time +from concurrent.futures import ThreadPoolExecutor + +import pytest + +from ludic.envs.code_exec.types import ( + BatchTestResult, + CompileResult, + CompileStatus, + ExecutionResult, + RunStatus, + TestCase, + TestResult, +) + +# Try to import LRUCache - skip all tests if docker package not installed +try: + from ludic.envs.code_exec.docker_sandbox import LRUCache +except ImportError: + LRUCache = None # type: ignore[misc, assignment] + +pytestmark = pytest.mark.skipif( + LRUCache is None, + reason="docker package not installed (required for LRUCache)", +) + + +def _make_batch_result(passed_count: int = 1, total_count: int = 1) -> BatchTestResult: + """Helper to create a BatchTestResult with minimal boilerplate.""" + results = [] + for i in range(total_count): + passed = i < passed_count + results.append( + TestResult( + test_case=TestCase(input=f"input_{i}", expected="out", id=f"t{i}"), + passed=passed, + actual="out" if passed else "wrong", + execution=ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS if passed else RunStatus.RUNTIME_ERROR, + ), + ) + ) + return BatchTestResult(results=results, code_hash="abc", tests_hash="xyz") + + +# --------------------------------------------------------------------- +# Basic Operations +# --------------------------------------------------------------------- + + +class TestLRUCacheBasicOperations: + def test_get_returns_none_for_missing_key(self): + cache = LRUCache(max_size=10) + result = cache.get("nonexistent_code", "nonexistent_tests") + assert result is None + + def test_put_and_get(self): + cache = LRUCache(max_size=10) + batch_result = _make_batch_result() + + cache.put("code1", "tests1", batch_result) + retrieved = cache.get("code1", "tests1") + + assert retrieved is batch_result + + def test_get_returns_none_after_different_key(self): + cache = LRUCache(max_size=10) + batch_result = _make_batch_result() + + cache.put("code1", "tests1", batch_result) + + # Different code hash + assert cache.get("code2", "tests1") is None + # Different tests hash + assert cache.get("code1", "tests2") is None + # Both different + assert cache.get("code2", "tests2") is None + + def test_put_overwrites_existing_entry(self): + cache = LRUCache(max_size=10) + result1 = _make_batch_result(passed_count=1, total_count=2) + result2 = _make_batch_result(passed_count=2, total_count=2) + + cache.put("code1", "tests1", result1) + cache.put("code1", "tests1", result2) + + retrieved = cache.get("code1", "tests1") + assert retrieved is result2 + assert retrieved.passed_count == 2 + + +# --------------------------------------------------------------------- +# Eviction Behavior +# --------------------------------------------------------------------- + + +class TestLRUCacheEviction: + def test_evicts_oldest_when_full(self): + cache = LRUCache(max_size=3) + + cache.put("code1", "tests", _make_batch_result()) + cache.put("code2", "tests", _make_batch_result()) + cache.put("code3", "tests", _make_batch_result()) + + # Cache is now full + assert cache.stats["size"] == 3 + + # Add one more - oldest (code1) should be evicted + cache.put("code4", "tests", _make_batch_result()) + + assert cache.stats["size"] == 3 + assert cache.get("code1", "tests") is None # Evicted + assert cache.get("code2", "tests") is not None + assert cache.get("code3", "tests") is not None + assert cache.get("code4", "tests") is not None + + def test_access_refreshes_entry_avoiding_eviction(self): + cache = LRUCache(max_size=3) + + cache.put("code1", "tests", _make_batch_result()) + cache.put("code2", "tests", _make_batch_result()) + cache.put("code3", "tests", _make_batch_result()) + + # Access code1 to make it most recently used + cache.get("code1", "tests") + + # Add new entry - code2 (now oldest accessed) should be evicted + cache.put("code4", "tests", _make_batch_result()) + + assert cache.get("code1", "tests") is not None # Still present + assert cache.get("code2", "tests") is None # Evicted + assert cache.get("code3", "tests") is not None + assert cache.get("code4", "tests") is not None + + def test_put_refreshes_existing_entry(self): + cache = LRUCache(max_size=3) + + cache.put("code1", "tests", _make_batch_result()) + cache.put("code2", "tests", _make_batch_result()) + cache.put("code3", "tests", _make_batch_result()) + + # Update code1 (makes it most recently used) + cache.put("code1", "tests", _make_batch_result()) + + # Add new entry - code2 should be evicted now + cache.put("code4", "tests", _make_batch_result()) + + assert cache.get("code1", "tests") is not None + assert cache.get("code2", "tests") is None # Evicted + assert cache.get("code3", "tests") is not None + assert cache.get("code4", "tests") is not None + + def test_max_size_one(self): + cache = LRUCache(max_size=1) + + cache.put("code1", "tests", _make_batch_result()) + assert cache.get("code1", "tests") is not None + + cache.put("code2", "tests", _make_batch_result()) + assert cache.get("code1", "tests") is None + assert cache.get("code2", "tests") is not None + + +# --------------------------------------------------------------------- +# Statistics Tracking +# --------------------------------------------------------------------- + + +class TestLRUCacheStats: + def test_initial_stats(self): + cache = LRUCache(max_size=100) + stats = cache.stats + + assert stats["hits"] == 0 + assert stats["misses"] == 0 + assert stats["size"] == 0 + assert stats["max_size"] == 100 + + def test_hit_tracking(self): + cache = LRUCache(max_size=10) + cache.put("code", "tests", _make_batch_result()) + + # First hit + cache.get("code", "tests") + assert cache.stats["hits"] == 1 + assert cache.stats["misses"] == 0 + + # Second hit + cache.get("code", "tests") + assert cache.stats["hits"] == 2 + assert cache.stats["misses"] == 0 + + def test_miss_tracking(self): + cache = LRUCache(max_size=10) + + # First miss + cache.get("nonexistent", "tests") + assert cache.stats["hits"] == 0 + assert cache.stats["misses"] == 1 + + # Second miss + cache.get("also_nonexistent", "tests") + assert cache.stats["hits"] == 0 + assert cache.stats["misses"] == 2 + + def test_mixed_hits_and_misses(self): + cache = LRUCache(max_size=10) + cache.put("code1", "tests", _make_batch_result()) + + cache.get("code1", "tests") # hit + cache.get("code2", "tests") # miss + cache.get("code1", "tests") # hit + cache.get("code3", "tests") # miss + cache.get("code1", "tests") # hit + + stats = cache.stats + assert stats["hits"] == 3 + assert stats["misses"] == 2 + + def test_size_tracking(self): + cache = LRUCache(max_size=10) + + assert cache.stats["size"] == 0 + + cache.put("code1", "tests", _make_batch_result()) + assert cache.stats["size"] == 1 + + cache.put("code2", "tests", _make_batch_result()) + assert cache.stats["size"] == 2 + + # Overwrite existing doesn't increase size + cache.put("code1", "tests", _make_batch_result()) + assert cache.stats["size"] == 2 + + +# --------------------------------------------------------------------- +# Thread Safety +# --------------------------------------------------------------------- + + +class TestLRUCacheThreadSafety: + def test_concurrent_puts(self): + cache = LRUCache(max_size=1000) + n_threads = 10 + puts_per_thread = 100 + + def put_items(thread_id: int): + for i in range(puts_per_thread): + cache.put(f"code_{thread_id}_{i}", "tests", _make_batch_result()) + + with ThreadPoolExecutor(max_workers=n_threads) as executor: + futures = [executor.submit(put_items, i) for i in range(n_threads)] + for f in futures: + f.result() + + # All items should be accessible + expected_size = n_threads * puts_per_thread + assert cache.stats["size"] == expected_size + + def test_concurrent_gets(self): + cache = LRUCache(max_size=100) + + # Pre-populate + for i in range(100): + cache.put(f"code_{i}", "tests", _make_batch_result()) + + n_threads = 10 + gets_per_thread = 100 + + def get_items(thread_id: int): + hits = 0 + for i in range(gets_per_thread): + key = f"code_{i % 100}" # Round-robin through existing keys + if cache.get(key, "tests") is not None: + hits += 1 + return hits + + with ThreadPoolExecutor(max_workers=n_threads) as executor: + futures = [executor.submit(get_items, i) for i in range(n_threads)] + results = [f.result() for f in futures] + + # All gets should have found their items + assert all(r == gets_per_thread for r in results) + + # Stats should reflect all hits + assert cache.stats["hits"] == n_threads * gets_per_thread + + def test_concurrent_mixed_operations(self): + cache = LRUCache(max_size=50) + n_threads = 8 + ops_per_thread = 100 + + errors = [] + + def mixed_operations(thread_id: int): + try: + for i in range(ops_per_thread): + if i % 3 == 0: + cache.put(f"code_{i}", "tests", _make_batch_result()) + else: + cache.get(f"code_{i % 30}", "tests") + # Access stats during operations + _ = cache.stats + except Exception as e: + errors.append(str(e)) + + with ThreadPoolExecutor(max_workers=n_threads) as executor: + futures = [executor.submit(mixed_operations, i) for i in range(n_threads)] + for f in futures: + f.result() + + # No errors should have occurred + assert len(errors) == 0, f"Errors during concurrent operations: {errors}" + + # Cache should be in a consistent state + stats = cache.stats + assert stats["size"] <= stats["max_size"] + assert stats["hits"] >= 0 + assert stats["misses"] >= 0 + + def test_concurrent_eviction_stress(self): + """Test that concurrent puts with eviction don't cause issues.""" + cache = LRUCache(max_size=10) + n_threads = 20 + puts_per_thread = 100 + + errors = [] + + def stress_puts(thread_id: int): + try: + for i in range(puts_per_thread): + cache.put(f"code_{thread_id}_{i}", "tests", _make_batch_result()) + except Exception as e: + errors.append(str(e)) + + with ThreadPoolExecutor(max_workers=n_threads) as executor: + futures = [executor.submit(stress_puts, i) for i in range(n_threads)] + for f in futures: + f.result() + + assert len(errors) == 0 + assert cache.stats["size"] == 10 # Should stay at max + + +# --------------------------------------------------------------------- +# Edge Cases +# --------------------------------------------------------------------- + + +class TestLRUCacheEdgeCases: + def test_empty_hash_strings(self): + cache = LRUCache(max_size=10) + batch = _make_batch_result() + + cache.put("", "", batch) + assert cache.get("", "") is batch + + def test_very_long_hash_strings(self): + cache = LRUCache(max_size=10) + batch = _make_batch_result() + + long_code_hash = "a" * 10000 + long_tests_hash = "b" * 10000 + + cache.put(long_code_hash, long_tests_hash, batch) + assert cache.get(long_code_hash, long_tests_hash) is batch + + def test_special_characters_in_hashes(self): + cache = LRUCache(max_size=10) + batch = _make_batch_result() + + special_hash = "!@#$%^&*()_+-=[]{}|;':\",./<>?" + cache.put(special_hash, special_hash, batch) + assert cache.get(special_hash, special_hash) is batch + + def test_unicode_in_hashes(self): + cache = LRUCache(max_size=10) + batch = _make_batch_result() + + unicode_hash = "hash_with_unicode_" + cache.put(unicode_hash, unicode_hash, batch) + assert cache.get(unicode_hash, unicode_hash) is batch diff --git a/tests/test_code_exec_env.py b/tests/test_code_exec_env.py new file mode 100644 index 0000000..70d3cb1 --- /dev/null +++ b/tests/test_code_exec_env.py @@ -0,0 +1,880 @@ +""" +Unit tests for ludic.envs.code_exec.env.CodeExecEnv + +Tests the environment with mock sandbox pools to avoid Docker dependency. +""" + +import pytest + +from ludic.envs.code_exec.env import CodeExecConfig, CodeExecEnv +from ludic.envs.code_exec.types import ( + BatchTestResult, + CompileResult, + CompileStatus, + ExecutionResult, + RunStatus, + TestCase, + TestResult, +) +from ludic.envs.code_exec.adapters.base import ExactMatchVerifier, TestAdapter +from ludic.envs.code_exec.sandbox import Sandbox, SandboxPool + + +# --------------------------------------------------------------------- +# Mock Implementations +# --------------------------------------------------------------------- + + +class MockSandbox: + """Mock sandbox for testing without Docker.""" + + def __init__( + self, + compile_result: CompileResult | None = None, + execute_results: dict[str, ExecutionResult] | None = None, + default_stdout: str = "", + ): + self._compile_result = compile_result or CompileResult( + status=CompileStatus.SUCCESS, + duration_ms=10.0, + ) + self._execute_results = execute_results or {} + self._default_stdout = default_stdout + self._python_version = "3.11" + + # Track calls + self.reset_calls = 0 + self.compile_calls: list[str] = [] + self.execute_calls: list[tuple[str, str]] = [] + + @property + def python_version(self) -> str: + return self._python_version + + async def reset(self) -> None: + self.reset_calls += 1 + + async def compile(self, code: str, *, timeout_s: float = 5.0) -> CompileResult: + self.compile_calls.append(code) + return self._compile_result + + async def execute( + self, + code: str, + *, + stdin: str = "", + skip_compile: bool = False, + timeout_s: float = 10.0, + memory_limit_mb: int | None = None, + env_vars: dict[str, str] | None = None, + ) -> ExecutionResult: + self.execute_calls.append((code, stdin)) + + if stdin in self._execute_results: + return self._execute_results[stdin] + + return ExecutionResult( + compile_result=self._compile_result, + run_status=RunStatus.SUCCESS, + stdout=self._default_stdout, + stderr="", + exit_code=0, + compile_duration_ms=10.0, + run_duration_ms=50.0, + total_duration_ms=60.0, + ) + + +class MockSandboxPool: + """Mock sandbox pool for testing without Docker.""" + + def __init__( + self, + sandbox: MockSandbox | None = None, + python_version: str = "3.11", + ): + self._sandbox = sandbox or MockSandbox() + self._python_version = python_version + self._cache: dict[tuple[str, str], BatchTestResult] = {} + + # Track calls + self.start_calls = 0 + self.checkout_calls = 0 + self.release_calls = 0 + self.shutdown_calls = 0 + + @property + def python_version(self) -> str: + return self._python_version + + async def start(self) -> None: + self.start_calls += 1 + + async def checkout(self, timeout_s: float = 30.0) -> Sandbox: + self.checkout_calls += 1 + return self._sandbox + + async def release(self, sandbox: Sandbox) -> None: + self.release_calls += 1 + + async def shutdown(self) -> None: + self.shutdown_calls += 1 + + def get_cached(self, code_hash: str, tests_hash: str) -> BatchTestResult | None: + return self._cache.get((code_hash, tests_hash)) + + def put_cached( + self, code_hash: str, tests_hash: str, result: BatchTestResult + ) -> None: + self._cache[(code_hash, tests_hash)] = result + + @property + def cache_stats(self) -> dict[str, int]: + """Return mock cache statistics.""" + return { + "hits": 0, + "misses": 0, + "size": len(self._cache), + "max_size": 10000, + } + + +class MockTestAdapter: + """Mock test adapter for testing.""" + + def __init__( + self, + prompt: str = "Write a program.", + problem_id: str = "test_problem", + tests: list[TestCase] | None = None, + ): + self._prompt = prompt + self._problem_id = problem_id + self._tests = tests or [ + TestCase(input="1", expected="1", id="test_0"), + ] + + def get_prompt(self, sample: dict) -> str: + return self._prompt + + def get_problem_id(self, sample: dict) -> str: + return self._problem_id + + def get_tests(self, sample: dict) -> list[TestCase]: + return self._tests + + def hash_tests(self, tests: list[TestCase]) -> str: + return "mock_tests_hash_1234" + + +# --------------------------------------------------------------------- +# Environment Reset Tests +# --------------------------------------------------------------------- + + +class TestCodeExecEnvReset: + @pytest.mark.asyncio + async def test_reset_returns_prompt_and_info(self): + sandbox = MockSandbox(default_stdout="1") + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter(prompt="Add two numbers.", problem_id="prob_1") + + env = CodeExecEnv( + sample={"question": "Add two numbers."}, + sandbox_pool=pool, + test_adapter=adapter, + ) + + obs, info = await env.env_reset() + + assert obs == "Add two numbers." + assert info["problem_id"] == "prob_1" + assert "num_tests" in info + assert "tests_hash" in info + assert "python_version" in info + + @pytest.mark.asyncio + async def test_reset_extracts_correct_number_of_tests(self): + sandbox = MockSandbox(default_stdout="out") + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter( + tests=[ + TestCase(input="1", expected="a", id="t0"), + TestCase(input="2", expected="b", id="t1"), + TestCase(input="3", expected="c", id="t2"), + ] + ) + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + ) + + obs, info = await env.env_reset() + + assert info["num_tests"] == 3 + + @pytest.mark.asyncio + async def test_reset_respects_max_tests_config(self): + sandbox = MockSandbox(default_stdout="out") + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter( + tests=[ + TestCase(input="1", expected="a", id="t0"), + TestCase(input="2", expected="b", id="t1"), + TestCase(input="3", expected="c", id="t2"), + TestCase(input="4", expected="d", id="t3"), + TestCase(input="5", expected="e", id="t4"), + ] + ) + + config = CodeExecConfig(max_tests=2) + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + config=config, + ) + + obs, info = await env.env_reset() + + assert info["num_tests"] == 2 + + @pytest.mark.asyncio + async def test_reset_handles_empty_tests(self): + sandbox = MockSandbox() + pool = MockSandboxPool(sandbox=sandbox) + + # Create adapter that returns empty tests + class EmptyTestsAdapter: + def get_prompt(self, sample: dict) -> str: + return "Write a program." + + def get_problem_id(self, sample: dict) -> str: + return "test_problem" + + def get_tests(self, sample: dict) -> list[TestCase]: + return [] # No tests! + + def hash_tests(self, tests: list[TestCase]) -> str: + return "empty_hash" + + adapter = EmptyTestsAdapter() + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + ) + + obs, info = await env.env_reset() + + assert "error" in info + assert info["error"] == "no_tests_extracted" + + @pytest.mark.asyncio + async def test_reset_sets_system_prompt(self): + sandbox = MockSandbox() + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter() + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + system_prompt="You are a Python expert.", + ) + + assert env.suggested_sysprompt == "You are a Python expert." + + +# --------------------------------------------------------------------- +# Environment Step Tests - Success Cases +# --------------------------------------------------------------------- + + +class TestCodeExecEnvStepSuccess: + @pytest.mark.asyncio + async def test_step_all_tests_pass(self): + sandbox = MockSandbox(default_stdout="expected_output") + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter( + tests=[ + TestCase(input="in1", expected="expected_output", id="t0"), + TestCase(input="in2", expected="expected_output", id="t1"), + ] + ) + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + ) + + await env.env_reset() + outcome = await env.env_step("print('expected_output')") + + assert outcome.terminated is True + assert outcome.truncated is False + assert outcome.reward == 1.0 + assert outcome.info["all_passed"] is True + assert outcome.info["passed"] == 2 + assert outcome.info["total"] == 2 + assert "All" in outcome.obs and "passed" in outcome.obs + + @pytest.mark.asyncio + async def test_step_releases_sandbox(self): + sandbox = MockSandbox(default_stdout="output") + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter( + tests=[TestCase(input="x", expected="output", id="t0")] + ) + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + ) + + await env.env_reset() + await env.env_step("code") + + assert pool.checkout_calls == 1 + assert pool.release_calls == 1 + + +# --------------------------------------------------------------------- +# Environment Step Tests - Failure Cases +# --------------------------------------------------------------------- + + +class TestCodeExecEnvStepFailure: + @pytest.mark.asyncio + async def test_step_without_reset_returns_error(self): + sandbox = MockSandbox() + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter() + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + ) + + # Skip reset + outcome = await env.env_step("some code") + + assert outcome.terminated is True + assert outcome.reward == -1.0 + assert outcome.info["error"] == "reset_not_called" + + @pytest.mark.asyncio + async def test_step_with_empty_code(self): + sandbox = MockSandbox() + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter() + config = CodeExecConfig(compile_failure_reward=-0.5) + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + config=config, + ) + + await env.env_reset() + outcome = await env.env_step("") + + assert outcome.terminated is True + assert outcome.reward == -0.5 + assert outcome.info["error"] == "empty_code" + + @pytest.mark.asyncio + async def test_step_with_whitespace_only_code(self): + sandbox = MockSandbox() + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter() + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + ) + + await env.env_reset() + outcome = await env.env_step(" \n\t ") + + assert outcome.info["error"] == "empty_code" + + @pytest.mark.asyncio + async def test_step_compile_failure(self): + compile_result = CompileResult( + status=CompileStatus.SYNTAX_ERROR, + error_message="SyntaxError: invalid syntax", + error_line=5, + duration_ms=10.0, + ) + sandbox = MockSandbox(compile_result=compile_result) + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter() + config = CodeExecConfig(compile_failure_reward=-0.2) + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + config=config, + ) + + await env.env_reset() + outcome = await env.env_step("def foo(") + + assert outcome.reward == -0.2 + assert outcome.info["compile_failed"] is True + assert "Compilation failed" in outcome.obs + assert "SyntaxError" in outcome.obs + + @pytest.mark.asyncio + async def test_step_some_tests_fail(self): + execute_results = { + "input1": ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + stdout="correct", + ), + "input2": ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + stdout="wrong", # Will fail + ), + } + sandbox = MockSandbox(execute_results=execute_results) + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter( + tests=[ + TestCase(input="input1", expected="correct", id="t0"), + TestCase(input="input2", expected="correct", id="t1"), + ] + ) + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + config=CodeExecConfig(stop_on_first_failure=False), + ) + + await env.env_reset() + outcome = await env.env_step("code") + + assert outcome.reward == 0.0 # Binary reward, not all passed + assert outcome.info["all_passed"] is False + assert outcome.info["passed"] == 1 + assert outcome.info["total"] == 2 + + +# --------------------------------------------------------------------- +# Reward Shaping Tests +# --------------------------------------------------------------------- + + +class TestCodeExecEnvRewardShaping: + @pytest.mark.asyncio + async def test_binary_reward_all_pass(self): + sandbox = MockSandbox(default_stdout="out") + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter( + tests=[TestCase(input="x", expected="out", id="t0")] + ) + config = CodeExecConfig(partial_credit=False) + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + config=config, + ) + + await env.env_reset() + outcome = await env.env_step("code") + + assert outcome.reward == 1.0 + + @pytest.mark.asyncio + async def test_binary_reward_some_fail(self): + execute_results = { + "in1": ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + stdout="correct", + ), + "in2": ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + stdout="wrong", + ), + } + sandbox = MockSandbox(execute_results=execute_results) + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter( + tests=[ + TestCase(input="in1", expected="correct", id="t0"), + TestCase(input="in2", expected="correct", id="t1"), + ] + ) + config = CodeExecConfig(partial_credit=False, stop_on_first_failure=False) + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + config=config, + ) + + await env.env_reset() + outcome = await env.env_step("code") + + assert outcome.reward == 0.0 # Binary: all or nothing + + @pytest.mark.asyncio + async def test_partial_credit_half_pass(self): + execute_results = { + "in1": ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + stdout="correct", + ), + "in2": ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + stdout="correct", + ), + "in3": ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + stdout="wrong", + ), + "in4": ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + stdout="wrong", + ), + } + sandbox = MockSandbox(execute_results=execute_results) + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter( + tests=[ + TestCase(input="in1", expected="correct", id="t0"), + TestCase(input="in2", expected="correct", id="t1"), + TestCase(input="in3", expected="correct", id="t2"), + TestCase(input="in4", expected="correct", id="t3"), + ] + ) + config = CodeExecConfig(partial_credit=True, stop_on_first_failure=False) + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + config=config, + ) + + await env.env_reset() + outcome = await env.env_step("code") + + assert outcome.reward == pytest.approx(0.5) # 2/4 passed + + +# --------------------------------------------------------------------- +# Caching Tests +# --------------------------------------------------------------------- + + +class TestCodeExecEnvCaching: + @pytest.mark.asyncio + async def test_cache_hit_skips_execution(self): + sandbox = MockSandbox(default_stdout="output") + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter( + tests=[TestCase(input="x", expected="output", id="t0")] + ) + config = CodeExecConfig(use_cache=True) + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + config=config, + ) + + await env.env_reset() + + # First call - should execute + outcome1 = await env.env_step("print('output')") + assert pool.checkout_calls == 1 + assert outcome1.info["cache_hit"] is False + + # Second call with same code - should hit cache + await env.env_reset() # Reset to allow another step + outcome2 = await env.env_step("print('output')") + assert pool.checkout_calls == 1 # No new checkout + assert outcome2.info["cache_hit"] is True + + @pytest.mark.asyncio + async def test_cache_disabled(self): + sandbox = MockSandbox(default_stdout="output") + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter( + tests=[TestCase(input="x", expected="output", id="t0")] + ) + config = CodeExecConfig(use_cache=False) + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + config=config, + ) + + await env.env_reset() + outcome1 = await env.env_step("print('output')") + assert pool.checkout_calls == 1 + + await env.env_reset() + outcome2 = await env.env_step("print('output')") + assert pool.checkout_calls == 2 # New execution each time + assert outcome2.info["cache_hit"] is False + + +# --------------------------------------------------------------------- +# Info Dict Tests +# --------------------------------------------------------------------- + + +class TestCodeExecEnvInfo: + @pytest.mark.asyncio + async def test_info_contains_required_fields(self): + sandbox = MockSandbox(default_stdout="out") + pool = MockSandboxPool(sandbox=sandbox, python_version="3.10") + adapter = MockTestAdapter(problem_id="prob_42") + adapter._tests = [TestCase(input="x", expected="out", id="t0")] + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + ) + + await env.env_reset() + outcome = await env.env_step("code") + info = outcome.info + + # Problem metadata + assert info["problem_id"] == "prob_42" + assert "code_hash" in info + assert "tests_hash" in info + + # Test results summary + assert "passed" in info + assert "total" in info + assert "all_passed" in info + assert "pass_rate" in info + assert "compile_failed" in info + + # Detailed results + assert "test_results" in info + assert isinstance(info["test_results"], list) + + # Timing + assert "timing" in info + assert "total_compile_ms" in info["timing"] + assert "total_run_ms" in info["timing"] + assert "total_execution_ms" in info["timing"] + + # Cache and env info + assert "cache_hit" in info + assert info["python_version"] == "3.10" + + @pytest.mark.asyncio + async def test_info_test_results_detail(self): + execute_results = { + "in1": ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + stdout="correct", + run_duration_ms=100.0, + ), + } + sandbox = MockSandbox(execute_results=execute_results) + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter( + tests=[TestCase(input="in1", expected="correct", id="test_001")] + ) + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + ) + + await env.env_reset() + outcome = await env.env_step("code") + + test_result = outcome.info["test_results"][0] + assert test_result["test_id"] == "test_001" + assert test_result["passed"] is True + assert test_result["compiled"] is True + assert test_result["ran"] is True + assert test_result["run_status"] == "success" + assert test_result["compile_status"] == "success" + + +# --------------------------------------------------------------------- +# Observation Building Tests +# --------------------------------------------------------------------- + + +class TestCodeExecEnvObservation: + @pytest.mark.asyncio + async def test_observation_on_success(self): + sandbox = MockSandbox(default_stdout="out") + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter( + tests=[ + TestCase(input="x", expected="out", id="t0"), + TestCase(input="y", expected="out", id="t1"), + ] + ) + + env = CodeExecEnv(sample={}, sandbox_pool=pool, test_adapter=adapter) + + await env.env_reset() + outcome = await env.env_step("code") + + assert "All 2 tests passed" in outcome.obs + + @pytest.mark.asyncio + async def test_observation_on_compile_error_includes_line(self): + compile_result = CompileResult( + status=CompileStatus.SYNTAX_ERROR, + error_message="invalid syntax", + error_line=42, + duration_ms=5.0, + ) + sandbox = MockSandbox(compile_result=compile_result) + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter() + + env = CodeExecEnv(sample={}, sandbox_pool=pool, test_adapter=adapter) + + await env.env_reset() + outcome = await env.env_step("bad code") + + assert "Compilation failed" in outcome.obs + assert "line 42" in outcome.obs + + @pytest.mark.asyncio + async def test_observation_truncates_long_errors(self): + long_error = "E" * 1000 + compile_result = CompileResult( + status=CompileStatus.SYNTAX_ERROR, + error_message=long_error, + duration_ms=5.0, + ) + sandbox = MockSandbox(compile_result=compile_result) + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter() + config = CodeExecConfig(max_error_length=100) + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + config=config, + ) + + await env.env_reset() + outcome = await env.env_step("code") + + # Error should be truncated with "..." + assert len(outcome.obs) < len(long_error) + assert "..." in outcome.obs + + @pytest.mark.asyncio + async def test_observation_includes_stderr_when_configured(self): + execute_results = { + "input": ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.RUNTIME_ERROR, + stdout="", + stderr="NameError: x is not defined", + ), + } + sandbox = MockSandbox(execute_results=execute_results) + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter( + tests=[TestCase(input="input", expected="output", id="t0")] + ) + config = CodeExecConfig(include_stderr_in_obs=True) + + env = CodeExecEnv( + sample={}, + sandbox_pool=pool, + test_adapter=adapter, + config=config, + ) + + await env.env_reset() + outcome = await env.env_step("print(x)") + + assert "Stderr" in outcome.obs + assert "NameError" in outcome.obs + + +# --------------------------------------------------------------------- +# Current Observation Tests +# --------------------------------------------------------------------- + + +class TestCodeExecEnvCurrentObs: + @pytest.mark.asyncio + async def test_env_current_obs_before_reset(self): + sandbox = MockSandbox() + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter() + + env = CodeExecEnv(sample={}, sandbox_pool=pool, test_adapter=adapter) + + obs = env.env_current_obs() + assert "Error" in obs + assert "reset" in obs.lower() + + @pytest.mark.asyncio + async def test_env_current_obs_after_reset(self): + sandbox = MockSandbox() + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter(prompt="Solve this problem.") + + env = CodeExecEnv(sample={}, sandbox_pool=pool, test_adapter=adapter) + + await env.env_reset() + obs = env.env_current_obs() + + assert obs == "Solve this problem." + + @pytest.mark.asyncio + async def test_env_current_obs_after_step(self): + sandbox = MockSandbox(default_stdout="result") + pool = MockSandboxPool(sandbox=sandbox) + adapter = MockTestAdapter( + tests=[TestCase(input="x", expected="result", id="t0")] + ) + + env = CodeExecEnv(sample={}, sandbox_pool=pool, test_adapter=adapter) + + await env.env_reset() + await env.env_step("code") + obs = env.env_current_obs() + + assert "passed" in obs diff --git a/tests/test_code_exec_podman.py b/tests/test_code_exec_podman.py new file mode 100644 index 0000000..301184d --- /dev/null +++ b/tests/test_code_exec_podman.py @@ -0,0 +1,546 @@ +""" +Unit tests for Podman-HPC sandbox implementation. + +These tests mock subprocess calls to test the logic without requiring +actual podman-hpc CLI or containers. +""" + +from __future__ import annotations + +import asyncio +import os +from dataclasses import dataclass +from typing import Optional +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from ludic.envs.code_exec.podman_sandbox import ( + LRUCache, + PodmanConfig, + PodmanError, + PodmanHPCSandbox, + PodmanHPCSandboxPool, + PodmanResult, + _get_container_name_prefix, +) +from ludic.envs.code_exec.backend import ( + SandboxBackend, + detect_available_backend, + is_docker_available, + is_podman_hpc_available, + is_singularity_available, + get_backend_info, +) +from ludic.envs.code_exec.types import ( + BatchTestResult, + CompileStatus, + RunStatus, + TestCase, + TestResult, + CompileResult, + ExecutionResult, +) + + +# ============================================================================ +# Container naming tests +# ============================================================================ + + +class TestContainerNaming: + """Tests for container name prefix generation.""" + + def test_local_prefix_without_slurm(self): + """Without SLURM_JOB_ID, should use 'local' prefix.""" + with patch.dict(os.environ, {}, clear=True): + # Ensure SLURM_JOB_ID is not set + os.environ.pop("SLURM_JOB_ID", None) + prefix = _get_container_name_prefix() + assert prefix == "ludic-sandbox-local" + + def test_slurm_prefix_with_job_id(self): + """With SLURM_JOB_ID, should include job ID in prefix.""" + with patch.dict(os.environ, {"SLURM_JOB_ID": "12345"}): + prefix = _get_container_name_prefix() + assert prefix == "ludic-sandbox-12345" + + +# ============================================================================ +# PodmanConfig tests +# ============================================================================ + + +class TestPodmanConfig: + """Tests for PodmanConfig dataclass.""" + + def test_default_config(self): + """Test default configuration values.""" + config = PodmanConfig() + assert config.memory_limit == "256m" + assert config.cpu_quota is None + assert config.network_disabled is True + assert config.working_dir == "/workspace" + assert config.gpu is False + assert config.extra_args is None + + def test_custom_config(self): + """Test custom configuration values.""" + config = PodmanConfig( + memory_limit="512m", + cpu_quota=0.5, + network_disabled=False, + gpu=True, + extra_args=["--security-opt", "label=disable"], + ) + assert config.memory_limit == "512m" + assert config.cpu_quota == 0.5 + assert config.network_disabled is False + assert config.gpu is True + assert config.extra_args == ["--security-opt", "label=disable"] + + +# ============================================================================ +# LRUCache tests (same as Docker implementation) +# ============================================================================ + + +class TestLRUCache: + """Tests for LRUCache implementation.""" + + def _make_batch_result(self, code_hash: str, tests_hash: str) -> BatchTestResult: + """Helper to create a BatchTestResult.""" + return BatchTestResult( + results=[], + code_hash=code_hash, + tests_hash=tests_hash, + ) + + def test_get_miss(self): + """Cache miss should return None and increment miss counter.""" + cache = LRUCache(max_size=10) + result = cache.get("code1", "tests1") + assert result is None + assert cache.stats["misses"] == 1 + assert cache.stats["hits"] == 0 + + def test_put_and_get(self): + """Should store and retrieve values.""" + cache = LRUCache(max_size=10) + batch_result = self._make_batch_result("code1", "tests1") + cache.put("code1", "tests1", batch_result) + + result = cache.get("code1", "tests1") + assert result is batch_result + assert cache.stats["hits"] == 1 + assert cache.stats["size"] == 1 + + def test_lru_eviction(self): + """Should evict least recently used when full.""" + cache = LRUCache(max_size=2) + + result1 = self._make_batch_result("code1", "tests1") + result2 = self._make_batch_result("code2", "tests2") + result3 = self._make_batch_result("code3", "tests3") + + cache.put("code1", "tests1", result1) + cache.put("code2", "tests2", result2) + # Access code1 to make it recently used + cache.get("code1", "tests1") + # Add code3, should evict code2 (least recently used) + cache.put("code3", "tests3", result3) + + assert cache.get("code1", "tests1") is result1 # Still there + assert cache.get("code2", "tests2") is None # Evicted + assert cache.get("code3", "tests3") is result3 # Still there + + def test_put_overwrites_existing(self): + """Should overwrite existing values with same key.""" + cache = LRUCache(max_size=10) + result1 = self._make_batch_result("code1", "tests1") + result2 = self._make_batch_result("code1", "tests1") + + cache.put("code1", "tests1", result1) + cache.put("code1", "tests1", result2) + + result = cache.get("code1", "tests1") + assert result is result2 + assert cache.stats["size"] == 1 + + +# ============================================================================ +# PodmanHPCSandbox tests (mocked subprocess) +# ============================================================================ + + +class TestPodmanHPCSandbox: + """Tests for PodmanHPCSandbox with mocked subprocess.""" + + @pytest.fixture + def sandbox(self): + """Create a sandbox instance for testing.""" + config = PodmanConfig(memory_limit="256m", network_disabled=True) + return PodmanHPCSandbox( + container_name="test-container", + image="python:3.11-slim", + config=config, + python_version="3.11", + ) + + @pytest.mark.asyncio + async def test_start_creates_container(self, sandbox): + """Start should create and run a persistent container.""" + mock_process = AsyncMock() + mock_process.returncode = 0 + mock_process.communicate = AsyncMock(return_value=(b"", b"")) + + with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: + await sandbox.start() + + # Should have called rm -f, run -d, and mkdir + assert mock_exec.call_count == 3 + calls = mock_exec.call_args_list + + # First call: rm -f + assert calls[0][0][0] == "podman-hpc" + assert "rm" in calls[0][0] + assert "-f" in calls[0][0] + + # Second call: run -d + assert calls[1][0][0] == "podman-hpc" + assert "run" in calls[1][0] + assert "-d" in calls[1][0] + assert "--name" in calls[1][0] + assert "test-container" in calls[1][0] + assert "sleep" in calls[1][0] + assert "infinity" in calls[1][0] + + # Third call: mkdir + assert calls[2][0][0] == "podman-hpc" + assert "exec" in calls[2][0] + assert "mkdir" in calls[2][0] + + @pytest.mark.asyncio + async def test_reset_clears_workspace(self, sandbox): + """Reset should clear the workspace directory.""" + sandbox._started = True + + mock_process = AsyncMock() + mock_process.returncode = 0 + mock_process.communicate = AsyncMock(return_value=(b"", b"")) + + with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: + await sandbox.reset() + + mock_exec.assert_called_once() + args = mock_exec.call_args[0] + assert "podman-hpc" in args + assert "exec" in args + assert "rm" in " ".join(args) + assert "/workspace/*" in " ".join(args) + + @pytest.mark.asyncio + async def test_compile_success(self, sandbox): + """Compile should return SUCCESS for valid code.""" + sandbox._started = True + + mock_process = AsyncMock() + mock_process.returncode = 0 + mock_process.communicate = AsyncMock(return_value=(b"", b"")) + + with patch("asyncio.create_subprocess_exec", return_value=mock_process): + result = await sandbox.compile("print('hello')") + + assert result.status == CompileStatus.SUCCESS + assert result.error_message is None + + @pytest.mark.asyncio + async def test_compile_syntax_error(self, sandbox): + """Compile should return SYNTAX_ERROR for invalid code.""" + sandbox._started = True + + error_output = b" File \"_check.py\", line 1\n def foo(\n ^\nSyntaxError: invalid syntax" + + # Create two different mock processes: + # 1. For _write_file (tar command) - should succeed + # 2. For py_compile - should fail with syntax error + write_process = AsyncMock() + write_process.returncode = 0 + write_process.communicate = AsyncMock(return_value=(b"", b"")) + + compile_process = AsyncMock() + compile_process.returncode = 1 + compile_process.communicate = AsyncMock(return_value=(b"", error_output)) + + call_count = [0] + def create_mock_process(*args, **kwargs): + call_count[0] += 1 + # First call is tar (write_file), second is py_compile + if call_count[0] == 1: + return write_process + return compile_process + + with patch("asyncio.create_subprocess_exec", side_effect=create_mock_process): + result = await sandbox.compile("def foo(") + + assert result.status == CompileStatus.SYNTAX_ERROR + assert "SyntaxError" in result.error_message + assert result.error_line == 1 + + @pytest.mark.asyncio + async def test_execute_success(self, sandbox): + """Execute should return SUCCESS and stdout for valid code.""" + sandbox._started = True + + # Mock two processes: one for compile (py_compile), one for execute + compile_process = AsyncMock() + compile_process.returncode = 0 + compile_process.communicate = AsyncMock(return_value=(b"", b"")) + + exec_process = AsyncMock() + exec_process.returncode = 0 + exec_process.communicate = AsyncMock(return_value=(b"hello world\n", b"")) + + call_count = [0] + def mock_create_subprocess(*args, **kwargs): + call_count[0] += 1 + # First few calls are for compile (write file, py_compile) + # Later calls are for execute (write file, run) + if "py_compile" in args or call_count[0] <= 2: + return compile_process + return exec_process + + with patch("asyncio.create_subprocess_exec", side_effect=mock_create_subprocess): + result = await sandbox.execute("print('hello world')") + + assert result.compiled + assert result.run_status == RunStatus.SUCCESS + assert "hello world" in result.stdout + + @pytest.mark.asyncio + async def test_execute_runtime_error(self, sandbox): + """Execute should return RUNTIME_ERROR for code that raises exception.""" + sandbox._started = True + + # Mock processes for various stages: + # 1. tar write (compile _write_file) + # 2. py_compile + # 3. tar write (execute _write_file) + # 4. python execution (runtime error) + success_process = AsyncMock() + success_process.returncode = 0 + success_process.communicate = AsyncMock(return_value=(b"", b"")) + + exec_process = AsyncMock() + exec_process.returncode = 1 + exec_process.communicate = AsyncMock(return_value=(b"", b"ZeroDivisionError: division by zero")) + + call_count = [0] + def mock_create_subprocess(*args, **kwargs): + call_count[0] += 1 + # Calls 1-3 are compile phase (tar, py_compile) and execute tar + # Call 4 is the actual execution + if call_count[0] <= 3: + return success_process + return exec_process + + with patch("asyncio.create_subprocess_exec", side_effect=mock_create_subprocess): + result = await sandbox.execute("1/0") + + assert result.compiled + assert result.run_status == RunStatus.RUNTIME_ERROR + assert "ZeroDivisionError" in result.stderr + + def test_parse_syntax_error(self): + """Test syntax error parsing.""" + error_msg = """ File "_check.py", line 5 + def foo( + ^ +SyntaxError: invalid syntax""" + + line, column, clean_msg = PodmanHPCSandbox._parse_syntax_error(error_msg) + + assert line == 5 + assert "SyntaxError" in clean_msg + assert "invalid syntax" in clean_msg + + +# ============================================================================ +# PodmanHPCSandboxPool tests +# ============================================================================ + + +class TestPodmanHPCSandboxPool: + """Tests for PodmanHPCSandboxPool.""" + + def test_parse_python_version_from_image(self): + """Should extract Python version from image name.""" + assert PodmanHPCSandboxPool._parse_python_version("python:3.11-slim") == "3.11" + assert PodmanHPCSandboxPool._parse_python_version("python:3.10") == "3.10" + assert PodmanHPCSandboxPool._parse_python_version("ghcr.io/foo/python:3.12-bullseye") == "3.12" + assert PodmanHPCSandboxPool._parse_python_version("custom-image:latest") == "3.11" # fallback + + def test_pool_initialization(self): + """Test pool initialization without starting.""" + pool = PodmanHPCSandboxPool( + n_workers=4, + image="python:3.11-slim", + cache_size=1000, + ) + + assert pool.python_version == "3.11" + assert pool.available == 0 # Not started yet + assert pool.cache_stats["size"] == 0 + + @pytest.mark.asyncio + async def test_checkout_before_start_raises(self): + """Checkout before start should raise RuntimeError.""" + pool = PodmanHPCSandboxPool(n_workers=2) + + with pytest.raises(RuntimeError, match="not started"): + await pool.checkout() + + @pytest.mark.asyncio + async def test_cache_operations(self): + """Test cache get/put operations.""" + pool = PodmanHPCSandboxPool(n_workers=2, cache_size=100) + + batch_result = BatchTestResult( + results=[], + code_hash="abc123", + tests_hash="def456", + ) + + # Cache miss + assert pool.get_cached("abc123", "def456") is None + + # Cache put + pool.put_cached("abc123", "def456", batch_result) + + # Cache hit + result = pool.get_cached("abc123", "def456") + assert result is batch_result + + +# ============================================================================ +# Backend detection tests +# ============================================================================ + + +class TestBackendDetection: + """Tests for backend detection functions.""" + + def test_sandbox_backend_enum(self): + """Test SandboxBackend enum values.""" + assert SandboxBackend.DOCKER.value == "docker" + assert SandboxBackend.PODMAN_HPC.value == "podman-hpc" + assert SandboxBackend.SINGULARITY.value == "singularity" + assert SandboxBackend.AUTO.value == "auto" + + def test_is_podman_hpc_available_not_installed(self): + """Should return False when podman-hpc is not in PATH.""" + with patch("shutil.which", return_value=None): + assert is_podman_hpc_available() is False + + def test_is_podman_hpc_available_installed(self): + """Should return True when podman-hpc is in PATH.""" + with patch("shutil.which", return_value="/usr/bin/podman-hpc"): + assert is_podman_hpc_available() is True + + def test_is_singularity_available_not_installed(self): + """Should return False when singularity is not in PATH.""" + with patch("shutil.which", return_value=None): + assert is_singularity_available() is False + + def test_is_singularity_available_installed(self): + """Should return True when singularity is in PATH.""" + def mock_which(cmd): + if cmd == "singularity": + return "/usr/bin/singularity" + return None + + with patch("shutil.which", side_effect=mock_which): + assert is_singularity_available() is True + + def test_is_singularity_available_apptainer(self): + """Should return True when apptainer (renamed singularity) is in PATH.""" + def mock_which(cmd): + if cmd == "apptainer": + return "/usr/bin/apptainer" + return None + + with patch("shutil.which", side_effect=mock_which): + assert is_singularity_available() is True + + def test_detect_backend_in_slurm_with_podman(self): + """In Slurm with podman-hpc available, should prefer podman-hpc.""" + with patch.dict(os.environ, {"SLURM_JOB_ID": "12345"}): + with patch("shutil.which", return_value="/usr/bin/podman-hpc"): + with patch("ludic.envs.code_exec.backend.is_docker_available", return_value=True): + backend = detect_available_backend() + assert backend == "podman-hpc" + + def test_detect_backend_outside_slurm_with_docker(self): + """Outside Slurm with Docker available, should prefer Docker.""" + with patch.dict(os.environ, {}, clear=True): + os.environ.pop("SLURM_JOB_ID", None) + with patch("ludic.envs.code_exec.backend.is_docker_available", return_value=True): + backend = detect_available_backend() + assert backend == "docker" + + def test_detect_backend_outside_slurm_no_docker_with_podman(self): + """Outside Slurm without Docker but with podman-hpc, should use podman-hpc.""" + with patch.dict(os.environ, {}, clear=True): + os.environ.pop("SLURM_JOB_ID", None) + with patch("ludic.envs.code_exec.backend.is_docker_available", return_value=False): + with patch("shutil.which", return_value="/usr/bin/podman-hpc"): + backend = detect_available_backend() + assert backend == "podman-hpc" + + def test_detect_backend_none_available_raises(self): + """Should raise RuntimeError when no backend is available.""" + with patch.dict(os.environ, {}, clear=True): + os.environ.pop("SLURM_JOB_ID", None) + with patch("ludic.envs.code_exec.backend.is_docker_available", return_value=False): + with patch("shutil.which", return_value=None): + with pytest.raises(RuntimeError, match="No sandbox backend available"): + detect_available_backend() + + def test_get_backend_info(self): + """Test get_backend_info returns structured data.""" + with patch.dict(os.environ, {"SLURM_JOB_ID": "99999"}): + with patch("ludic.envs.code_exec.backend.is_docker_available", return_value=False): + with patch("shutil.which", return_value="/usr/bin/podman-hpc"): + info = get_backend_info() + + assert info["environment"]["in_slurm"] is True + assert info["environment"]["slurm_job_id"] == "99999" + assert "docker" in info["backends"] + assert "podman-hpc" in info["backends"] + assert info["backends"]["podman-hpc"]["available"] is True + assert info["backends"]["docker"]["available"] is False + + +# ============================================================================ +# Factory tests +# ============================================================================ + + +class TestFactory: + """Tests for create_sandbox_pool factory.""" + + @pytest.mark.asyncio + async def test_factory_unknown_backend_raises(self): + """Factory should raise ValueError for unknown backend.""" + from ludic.envs.code_exec.factory import create_sandbox_pool + + with pytest.raises(ValueError, match="Unknown backend"): + await create_sandbox_pool(backend="unknown") + + @pytest.mark.asyncio + async def test_factory_singularity_not_implemented(self): + """Factory should raise NotImplementedError for singularity.""" + from ludic.envs.code_exec.factory import create_sandbox_pool + + with pytest.raises(NotImplementedError, match="Singularity backend is not yet implemented"): + await create_sandbox_pool(backend="singularity") diff --git a/tests/test_code_exec_runners.py b/tests/test_code_exec_runners.py new file mode 100644 index 0000000..7853c4c --- /dev/null +++ b/tests/test_code_exec_runners.py @@ -0,0 +1,478 @@ +""" +Unit tests for ludic.envs.code_exec.runners + +Tests hash utilities and StdinStdoutRunner with mock sandbox. +""" + +import pytest + +from ludic.envs.code_exec.runners import ( + compute_hash, + hash_tests, + StdinStdoutRunner, +) +from ludic.envs.code_exec.types import ( + TestCase, + CompileResult, + CompileStatus, + ExecutionResult, + RunStatus, +) +from ludic.envs.code_exec.adapters.base import ExactMatchVerifier + + +# --------------------------------------------------------------------- +# Hash Utility Tests +# --------------------------------------------------------------------- + + +class TestComputeHash: + def test_returns_16_chars(self): + result = compute_hash("hello world") + assert len(result) == 16 + + def test_deterministic(self): + result1 = compute_hash("test content") + result2 = compute_hash("test content") + assert result1 == result2 + + def test_different_content_different_hash(self): + result1 = compute_hash("content a") + result2 = compute_hash("content b") + assert result1 != result2 + + def test_hex_characters_only(self): + result = compute_hash("any content") + assert all(c in "0123456789abcdef" for c in result) + + def test_empty_string(self): + result = compute_hash("") + assert len(result) == 16 + + +class TestHashTests: + def test_returns_16_chars(self): + tests = [TestCase(input="1", expected="2", id="t1")] + result = hash_tests(tests) + assert len(result) == 16 + + def test_deterministic(self): + tests = [ + TestCase(input="1", expected="a", id="t1"), + TestCase(input="2", expected="b", id="t2"), + ] + result1 = hash_tests(tests) + result2 = hash_tests(tests) + assert result1 == result2 + + def test_different_tests_different_hash(self): + tests1 = [TestCase(input="1", expected="a", id="t1")] + tests2 = [TestCase(input="2", expected="b", id="t2")] + result1 = hash_tests(tests1) + result2 = hash_tests(tests2) + assert result1 != result2 + + def test_order_matters(self): + tests1 = [ + TestCase(input="1", expected="a", id="t1"), + TestCase(input="2", expected="b", id="t2"), + ] + tests2 = [ + TestCase(input="2", expected="b", id="t2"), + TestCase(input="1", expected="a", id="t1"), + ] + result1 = hash_tests(tests1) + result2 = hash_tests(tests2) + assert result1 != result2 + + def test_empty_list(self): + result = hash_tests([]) + assert len(result) == 16 + + +# --------------------------------------------------------------------- +# Mock Sandbox for Runner Tests +# --------------------------------------------------------------------- + + +class MockSandbox: + """ + A mock sandbox for testing runners. + + Can be configured with: + - compile_result: What to return from compile() + - execute_results: Dict mapping stdin -> ExecutionResult + - default_execute_result: Fallback for unmapped stdin + """ + + def __init__( + self, + compile_result: CompileResult | None = None, + execute_results: dict[str, ExecutionResult] | None = None, + default_stdout: str = "", + ): + self._compile_result = compile_result or CompileResult( + status=CompileStatus.SUCCESS, + duration_ms=10.0, + ) + self._execute_results = execute_results or {} + self._default_stdout = default_stdout + self._python_version = "3.11" + + # Track calls for assertions + self.compile_calls: list[str] = [] + self.execute_calls: list[tuple[str, str]] = [] + + @property + def python_version(self) -> str: + return self._python_version + + async def reset(self) -> None: + pass + + async def compile(self, code: str, *, timeout_s: float = 5.0) -> CompileResult: + self.compile_calls.append(code) + return self._compile_result + + async def execute( + self, + code: str, + *, + stdin: str = "", + skip_compile: bool = False, + timeout_s: float = 10.0, + memory_limit_mb: int | None = None, + env_vars: dict[str, str] | None = None, + ) -> ExecutionResult: + self.execute_calls.append((code, stdin)) + + if stdin in self._execute_results: + return self._execute_results[stdin] + + # Default: successful execution returning default_stdout + return ExecutionResult( + compile_result=self._compile_result, + run_status=RunStatus.SUCCESS, + stdout=self._default_stdout, + stderr="", + exit_code=0, + compile_duration_ms=10.0, + run_duration_ms=50.0, + total_duration_ms=60.0, + ) + + +# --------------------------------------------------------------------- +# StdinStdoutRunner Tests +# --------------------------------------------------------------------- + + +class TestStdinStdoutRunner: + @pytest.mark.asyncio + async def test_all_tests_pass(self): + sandbox = MockSandbox(default_stdout="expected_output") + runner = StdinStdoutRunner() + verifier = ExactMatchVerifier() + + tests = [ + TestCase(input="input1", expected="expected_output", id="t1"), + TestCase(input="input2", expected="expected_output", id="t2"), + ] + + result = await runner.run_tests( + sandbox=sandbox, + code="print('expected_output')", + tests=tests, + verifier=verifier, + ) + + assert result.all_passed is True + assert result.passed_count == 2 + assert result.total_count == 2 + + @pytest.mark.asyncio + async def test_some_tests_fail(self): + # First test passes, second fails + execute_results = { + "input1": ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + stdout="correct", + ), + "input2": ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + stdout="wrong", + ), + } + sandbox = MockSandbox(execute_results=execute_results) + runner = StdinStdoutRunner() + verifier = ExactMatchVerifier() + + tests = [ + TestCase(input="input1", expected="correct", id="t1"), + TestCase(input="input2", expected="correct", id="t2"), # Will fail + ] + + result = await runner.run_tests( + sandbox=sandbox, + code="code", + tests=tests, + verifier=verifier, + ) + + assert result.all_passed is False + assert result.passed_count == 1 + assert result.total_count == 2 + assert result.results[0].passed is True + assert result.results[1].passed is False + + @pytest.mark.asyncio + async def test_compile_failure_fails_all_tests(self): + compile_result = CompileResult( + status=CompileStatus.SYNTAX_ERROR, + error_message="SyntaxError: invalid syntax", + error_line=5, + duration_ms=5.0, + ) + sandbox = MockSandbox(compile_result=compile_result) + runner = StdinStdoutRunner() + verifier = ExactMatchVerifier() + + tests = [ + TestCase(input="input1", expected="x", id="t1"), + TestCase(input="input2", expected="y", id="t2"), + TestCase(input="input3", expected="z", id="t3"), + ] + + result = await runner.run_tests( + sandbox=sandbox, + code="invalid syntax here", + tests=tests, + verifier=verifier, + compile_first=True, + ) + + assert result.compile_failed is True + assert result.all_passed is False + assert result.passed_count == 0 + assert len(result.results) == 3 + + # All should have compile failure details + for r in result.results: + assert r.compiled is False + assert "Compilation failed" in (r.comparison_details or "") + + @pytest.mark.asyncio + async def test_stop_on_first_failure(self): + execute_results = { + "input1": ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + stdout="wrong", # First test fails + ), + } + sandbox = MockSandbox(execute_results=execute_results, default_stdout="correct") + runner = StdinStdoutRunner() + verifier = ExactMatchVerifier() + + tests = [ + TestCase(input="input1", expected="correct", id="t1"), # Fails + TestCase(input="input2", expected="correct", id="t2"), # Should be skipped + TestCase(input="input3", expected="correct", id="t3"), # Should be skipped + ] + + result = await runner.run_tests( + sandbox=sandbox, + code="code", + tests=tests, + verifier=verifier, + stop_on_first_failure=True, + ) + + assert result.passed_count == 0 + assert len(result.results) == 3 + + # First test ran and failed + assert result.results[0].passed is False + assert result.results[0].ran is True + + # Second and third were skipped + assert result.results[1].passed is False + assert result.results[1].execution.run_status == RunStatus.NOT_RUN + assert result.results[2].passed is False + assert result.results[2].execution.run_status == RunStatus.NOT_RUN + + @pytest.mark.asyncio + async def test_runtime_error_fails_test(self): + execute_results = { + "input1": ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.RUNTIME_ERROR, + stdout="", + stderr="NameError: name 'x' is not defined", + exit_code=1, + ), + } + sandbox = MockSandbox(execute_results=execute_results) + runner = StdinStdoutRunner() + verifier = ExactMatchVerifier() + + tests = [TestCase(input="input1", expected="output", id="t1")] + + result = await runner.run_tests( + sandbox=sandbox, + code="print(x)", + tests=tests, + verifier=verifier, + ) + + assert result.passed_count == 0 + assert result.results[0].passed is False + assert "Runtime error" in (result.results[0].comparison_details or "") + + @pytest.mark.asyncio + async def test_timeout_fails_test(self): + execute_results = { + "input1": ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.TIMEOUT, + stdout="", + stderr="", + run_duration_ms=5000.0, + ), + } + sandbox = MockSandbox(execute_results=execute_results) + runner = StdinStdoutRunner() + verifier = ExactMatchVerifier() + + tests = [TestCase(input="input1", expected="output", id="t1")] + + result = await runner.run_tests( + sandbox=sandbox, + code="while True: pass", + tests=tests, + verifier=verifier, + ) + + assert result.passed_count == 0 + assert result.results[0].passed is False + assert "timed out" in (result.results[0].comparison_details or "").lower() + + @pytest.mark.asyncio + async def test_memory_exceeded_fails_test(self): + execute_results = { + "input1": ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.MEMORY_EXCEEDED, + stdout="", + stderr="", + ), + } + sandbox = MockSandbox(execute_results=execute_results) + runner = StdinStdoutRunner() + verifier = ExactMatchVerifier() + + tests = [TestCase(input="input1", expected="output", id="t1")] + + result = await runner.run_tests( + sandbox=sandbox, + code="x = [0] * 10**9", + tests=tests, + verifier=verifier, + ) + + assert result.passed_count == 0 + assert result.results[0].passed is False + assert "Memory" in (result.results[0].comparison_details or "") + + @pytest.mark.asyncio + async def test_per_test_timeout_override(self): + sandbox = MockSandbox(default_stdout="output") + runner = StdinStdoutRunner(default_timeout_s=5.0) + verifier = ExactMatchVerifier() + + tests = [ + TestCase( + input="input1", + expected="output", + id="t1", + metadata={"timeout_s": 30.0}, # Override + ), + ] + + await runner.run_tests( + sandbox=sandbox, + code="code", + tests=tests, + verifier=verifier, + ) + + # Check that execute was called with the overridden timeout + # The mock doesn't actually use timeout, but we can verify the call was made + assert len(sandbox.execute_calls) == 1 + + @pytest.mark.asyncio + async def test_compile_first_false_skips_compile(self): + sandbox = MockSandbox(default_stdout="output") + runner = StdinStdoutRunner() + verifier = ExactMatchVerifier() + + tests = [TestCase(input="input1", expected="output", id="t1")] + + await runner.run_tests( + sandbox=sandbox, + code="code", + tests=tests, + verifier=verifier, + compile_first=False, + ) + + # compile() should not be called when compile_first=False + assert len(sandbox.compile_calls) == 0 + assert len(sandbox.execute_calls) == 1 + + @pytest.mark.asyncio + async def test_hashes_computed_correctly(self): + sandbox = MockSandbox(default_stdout="output") + runner = StdinStdoutRunner() + verifier = ExactMatchVerifier() + + code = "print('hello')" + tests = [TestCase(input="input1", expected="output", id="t1")] + + result = await runner.run_tests( + sandbox=sandbox, + code=code, + tests=tests, + verifier=verifier, + ) + + # Verify hashes are present and have correct format + assert len(result.code_hash) == 16 + assert len(result.tests_hash) == 16 + assert all(c in "0123456789abcdef" for c in result.code_hash) + assert all(c in "0123456789abcdef" for c in result.tests_hash) + + # Verify code_hash matches compute_hash + from ludic.envs.code_exec.runners import compute_hash + + assert result.code_hash == compute_hash(code) + + @pytest.mark.asyncio + async def test_whitespace_stripping_in_comparison(self): + """Verifier should strip whitespace from output.""" + sandbox = MockSandbox(default_stdout=" output\n") + runner = StdinStdoutRunner() + verifier = ExactMatchVerifier() # strips by default + + tests = [TestCase(input="input1", expected="output", id="t1")] + + result = await runner.run_tests( + sandbox=sandbox, + code="code", + tests=tests, + verifier=verifier, + ) + + assert result.all_passed is True diff --git a/tests/test_code_exec_types.py b/tests/test_code_exec_types.py new file mode 100644 index 0000000..edffc08 --- /dev/null +++ b/tests/test_code_exec_types.py @@ -0,0 +1,400 @@ +""" +Unit tests for ludic.envs.code_exec.types + +Tests all dataclasses and their properties/methods. +""" + +import pytest + +from ludic.envs.code_exec.types import ( + CompileStatus, + RunStatus, + CompileResult, + ExecutionResult, + TestCase, + TestResult, + BatchTestResult, +) + + +# --------------------------------------------------------------------- +# CompileResult Tests +# --------------------------------------------------------------------- + + +class TestCompileResult: + def test_success_property_true_when_status_success(self): + result = CompileResult(status=CompileStatus.SUCCESS) + assert result.success is True + + def test_success_property_false_when_syntax_error(self): + result = CompileResult( + status=CompileStatus.SYNTAX_ERROR, + error_message="SyntaxError: invalid syntax", + error_line=5, + error_column=10, + ) + assert result.success is False + + def test_success_property_false_for_all_error_statuses(self): + error_statuses = [ + CompileStatus.SYNTAX_ERROR, + CompileStatus.IMPORT_ERROR, + CompileStatus.TIMEOUT, + CompileStatus.UNKNOWN_ERROR, + ] + for status in error_statuses: + result = CompileResult(status=status) + assert result.success is False, f"Expected success=False for {status}" + + def test_duration_ms_default_zero(self): + result = CompileResult(status=CompileStatus.SUCCESS) + assert result.duration_ms == 0.0 + + +# --------------------------------------------------------------------- +# ExecutionResult Tests +# --------------------------------------------------------------------- + + +class TestExecutionResult: + def test_compiled_true_when_compile_succeeded(self): + result = ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + ) + assert result.compiled is True + + def test_compiled_false_when_compile_failed(self): + result = ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SYNTAX_ERROR), + ) + assert result.compiled is False + + def test_succeeded_true_when_compiled_and_run_success(self): + result = ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + stdout="output", + ) + assert result.succeeded is True + + def test_succeeded_false_when_compile_failed(self): + result = ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SYNTAX_ERROR), + ) + assert result.succeeded is False + + def test_succeeded_false_when_runtime_error(self): + result = ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.RUNTIME_ERROR, + stderr="NameError: name 'x' is not defined", + ) + assert result.succeeded is False + + def test_succeeded_false_when_timeout(self): + result = ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.TIMEOUT, + ) + assert result.succeeded is False + + def test_timed_out_true_when_compile_timeout(self): + result = ExecutionResult( + compile_result=CompileResult(status=CompileStatus.TIMEOUT), + ) + assert result.timed_out is True + + def test_timed_out_true_when_run_timeout(self): + result = ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.TIMEOUT, + ) + assert result.timed_out is True + + def test_timed_out_false_when_success(self): + result = ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + ) + assert result.timed_out is False + + def test_default_values(self): + result = ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + ) + assert result.run_status is None + assert result.stdout == "" + assert result.stderr == "" + assert result.exit_code is None + assert result.cache_hit is False + + +# --------------------------------------------------------------------- +# TestCase Tests +# --------------------------------------------------------------------- + + +class TestTestCase: + def test_basic_creation(self): + tc = TestCase(input="1 2", expected="3", id="test_add") + assert tc.input == "1 2" + assert tc.expected == "3" + assert tc.id == "test_add" + + def test_default_weight(self): + tc = TestCase(input="x", expected="y") + assert tc.weight == 1.0 + + def test_default_metadata_is_empty_dict(self): + tc = TestCase(input="x", expected="y") + assert tc.metadata == {} + + def test_metadata_with_custom_values(self): + tc = TestCase( + input="x", + expected="y", + metadata={"timeout_s": 10.0, "category": "math"}, + ) + assert tc.metadata["timeout_s"] == 10.0 + assert tc.metadata["category"] == "math" + + +# --------------------------------------------------------------------- +# TestResult Tests +# --------------------------------------------------------------------- + + +class TestTestResult: + def test_compiled_delegates_to_execution(self): + execution = ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + ) + tr = TestResult( + test_case=TestCase(input="1", expected="1"), + passed=True, + actual="1", + execution=execution, + ) + assert tr.compiled is True + + def test_compiled_false_when_execution_compile_failed(self): + execution = ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SYNTAX_ERROR), + ) + tr = TestResult( + test_case=TestCase(input="1", expected="1"), + passed=False, + actual="", + execution=execution, + ) + assert tr.compiled is False + + def test_ran_true_when_execution_has_run_status(self): + execution = ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.SUCCESS, + ) + tr = TestResult( + test_case=TestCase(input="1", expected="1"), + passed=True, + actual="1", + execution=execution, + ) + assert tr.ran is True + + def test_ran_false_when_run_status_none(self): + execution = ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SYNTAX_ERROR), + run_status=None, + ) + tr = TestResult( + test_case=TestCase(input="1", expected="1"), + passed=False, + actual="", + execution=execution, + ) + assert tr.ran is False + + def test_ran_false_when_run_status_not_run(self): + execution = ExecutionResult( + compile_result=CompileResult(status=CompileStatus.SUCCESS), + run_status=RunStatus.NOT_RUN, + ) + tr = TestResult( + test_case=TestCase(input="1", expected="1"), + passed=False, + actual="", + execution=execution, + ) + assert tr.ran is False + + +# --------------------------------------------------------------------- +# BatchTestResult Tests +# --------------------------------------------------------------------- + + +def _make_test_result(passed: bool, compiled: bool = True) -> TestResult: + """Helper to create TestResult with minimal boilerplate.""" + if compiled: + compile_result = CompileResult(status=CompileStatus.SUCCESS) + run_status = RunStatus.SUCCESS if passed else RunStatus.RUNTIME_ERROR + else: + compile_result = CompileResult(status=CompileStatus.SYNTAX_ERROR) + run_status = None + + return TestResult( + test_case=TestCase(input="x", expected="y"), + passed=passed, + actual="y" if passed else "z", + execution=ExecutionResult( + compile_result=compile_result, + run_status=run_status, + ), + ) + + +class TestBatchTestResult: + def test_passed_count(self): + results = [ + _make_test_result(passed=True), + _make_test_result(passed=True), + _make_test_result(passed=False), + ] + batch = BatchTestResult(results=results, code_hash="abc", tests_hash="xyz") + assert batch.passed_count == 2 + + def test_total_count(self): + results = [_make_test_result(passed=True) for _ in range(5)] + batch = BatchTestResult(results=results, code_hash="abc", tests_hash="xyz") + assert batch.total_count == 5 + + def test_all_passed_true_when_all_pass(self): + results = [_make_test_result(passed=True) for _ in range(3)] + batch = BatchTestResult(results=results, code_hash="abc", tests_hash="xyz") + assert batch.all_passed is True + + def test_all_passed_false_when_one_fails(self): + results = [ + _make_test_result(passed=True), + _make_test_result(passed=False), + ] + batch = BatchTestResult(results=results, code_hash="abc", tests_hash="xyz") + assert batch.all_passed is False + + def test_all_passed_false_when_empty(self): + batch = BatchTestResult(results=[], code_hash="abc", tests_hash="xyz") + assert batch.all_passed is False + + def test_pass_rate_full(self): + results = [_make_test_result(passed=True) for _ in range(4)] + batch = BatchTestResult(results=results, code_hash="abc", tests_hash="xyz") + assert batch.pass_rate == pytest.approx(1.0) + + def test_pass_rate_half(self): + results = [ + _make_test_result(passed=True), + _make_test_result(passed=True), + _make_test_result(passed=False), + _make_test_result(passed=False), + ] + batch = BatchTestResult(results=results, code_hash="abc", tests_hash="xyz") + assert batch.pass_rate == pytest.approx(0.5) + + def test_pass_rate_zero_when_empty(self): + batch = BatchTestResult(results=[], code_hash="abc", tests_hash="xyz") + assert batch.pass_rate == pytest.approx(0.0) + + def test_first_failure_returns_first_failed_test(self): + results = [ + _make_test_result(passed=True), + _make_test_result(passed=False), # first failure + _make_test_result(passed=False), + ] + batch = BatchTestResult(results=results, code_hash="abc", tests_hash="xyz") + assert batch.first_failure is results[1] + + def test_first_failure_none_when_all_pass(self): + results = [_make_test_result(passed=True) for _ in range(3)] + batch = BatchTestResult(results=results, code_hash="abc", tests_hash="xyz") + assert batch.first_failure is None + + def test_compile_failed_true_when_first_result_not_compiled(self): + results = [ + _make_test_result(passed=False, compiled=False), + _make_test_result(passed=False, compiled=False), + ] + batch = BatchTestResult(results=results, code_hash="abc", tests_hash="xyz") + assert batch.compile_failed is True + + def test_compile_failed_false_when_compiled(self): + results = [_make_test_result(passed=True)] + batch = BatchTestResult(results=results, code_hash="abc", tests_hash="xyz") + assert batch.compile_failed is False + + def test_compile_failed_false_when_empty(self): + batch = BatchTestResult(results=[], code_hash="abc", tests_hash="xyz") + assert batch.compile_failed is False + + def test_get_failures_returns_only_failed_tests(self): + results = [ + _make_test_result(passed=True), + _make_test_result(passed=False), + _make_test_result(passed=True), + _make_test_result(passed=False), + ] + batch = BatchTestResult(results=results, code_hash="abc", tests_hash="xyz") + failures = batch.get_failures() + assert len(failures) == 2 + assert failures[0] is results[1] + assert failures[1] is results[3] + + def test_get_successes_returns_only_passed_tests(self): + results = [ + _make_test_result(passed=True), + _make_test_result(passed=False), + _make_test_result(passed=True), + ] + batch = BatchTestResult(results=results, code_hash="abc", tests_hash="xyz") + successes = batch.get_successes() + assert len(successes) == 2 + assert successes[0] is results[0] + assert successes[1] is results[2] + + def test_timing_aggregation(self): + # Create results with specific timing + def make_result_with_timing(compile_ms: float, run_ms: float) -> TestResult: + return TestResult( + test_case=TestCase(input="x", expected="y"), + passed=True, + actual="y", + execution=ExecutionResult( + compile_result=CompileResult( + status=CompileStatus.SUCCESS, + duration_ms=compile_ms, + ), + run_status=RunStatus.SUCCESS, + compile_duration_ms=compile_ms, + run_duration_ms=run_ms, + total_duration_ms=compile_ms + run_ms, + ), + ) + + results = [ + make_result_with_timing(10.0, 100.0), + make_result_with_timing(10.0, 200.0), + make_result_with_timing(10.0, 150.0), + ] + batch = BatchTestResult(results=results, code_hash="abc", tests_hash="xyz") + + # Compile time: max across all (since compilation usually happens once) + assert batch.total_compile_ms == pytest.approx(10.0) + + # Run time: sum across all tests + assert batch.total_run_ms == pytest.approx(450.0) # 100 + 200 + 150 + + # Total: sum of all total_duration_ms + assert batch.total_execution_ms == pytest.approx(480.0) # 110 + 210 + 160 diff --git a/tests/test_flash_attention.py b/tests/test_flash_attention.py new file mode 100644 index 0000000..fa33e07 --- /dev/null +++ b/tests/test_flash_attention.py @@ -0,0 +1,151 @@ +""" +GPU tests for Flash Attention and hardware detection. + +These tests are designed to run on interactive GPU nodes (not login nodes). +Mark with @pytest.mark.gpu and run with: pytest -v -m gpu + +Usage on Isambard: + srun --nodes=1 --gpus=1 --time=10:00 --pty bash + uv run pytest tests/test_flash_attention.py -v -m gpu -s +""" + +from __future__ import annotations + +import logging +import pytest +import torch + +# Configure logging for visibility during tests +logging.basicConfig(level=logging.DEBUG, format="%(name)s: %(message)s") + + + +@pytest.mark.gpu +def test_cuda_available(): + """Verify CUDA is available (basic sanity check).""" + assert torch.cuda.is_available(), "CUDA not available - run on a GPU node" + + +@pytest.mark.gpu +def test_flash_sdp_enabled(): + """Verify Flash SDP backend can be enabled.""" + torch.backends.cuda.enable_flash_sdp(True) + # Note: flash_sdp_enabled() returns True only if flash kernels are actually usable + # This depends on the input shapes and dtypes at runtime + assert hasattr(torch.backends.cuda, "flash_sdp_enabled") + + +@pytest.mark.gpu +def test_detect_gpu_architecture(): + """Detect real GPU architecture.""" + from ludic.training.hardware import detect_gpu_architecture + + arch = detect_gpu_architecture() + assert arch is not None, "Could not detect GPU architecture" + + # Log the detected architecture + device_name = torch.cuda.get_device_name() + capability = torch.cuda.get_device_capability() + print(f"GPU: {device_name}") + print(f"Compute capability: sm_{capability[0]}{capability[1]}") + print(f"Detected architecture: {arch}") + + # Validate known architectures + assert arch in ("hopper", "ampere", "ada", "turing", "volta", "older") + + +@pytest.mark.gpu +def test_get_cuda_version(): + """Verify CUDA version detection.""" + from ludic.training.hardware import get_cuda_version + + version = get_cuda_version() + assert version is not None, "Could not get CUDA version" + + major, minor = version + print(f"CUDA version: {major}.{minor}") + + # Reasonable version bounds + assert major >= 11, f"CUDA version {major}.{minor} is too old for Flash Attention" + + +@pytest.mark.gpu +def test_flash_attn_import(): + """Verify flash-attn package loads and reports version.""" + try: + import flash_attn + version = flash_attn.__version__ + print(f"flash-attn version: {version}") + + # Check version is >= 2.7.0 for FA3 support + parts = version.split(".") + major, minor = int(parts[0]), int(parts[1]) + assert (major, minor) >= (2, 7), f"flash-attn {version} < 2.7.0, FA3 not supported" + + except ImportError as e: + pytest.skip(f"flash-attn not installed: {e}") + + +@pytest.mark.gpu +def test_get_optimal_attention_impl(): + """Test optimal attention implementation selection.""" + from ludic.training.hardware import get_optimal_attention_impl + + # With flash attention enabled (default) + impl = get_optimal_attention_impl(disable_flash_attn=False) + print(f"Optimal attention (enabled): {impl}") + assert impl in ("flash_attention_3", "flash_attention_2", "sdpa", "eager") + + # With flash attention disabled + impl_disabled = get_optimal_attention_impl(disable_flash_attn=True) + print(f"Optimal attention (disabled): {impl_disabled}") + assert impl_disabled == "sdpa" + + +@pytest.mark.gpu +def test_configure_flash_attention(): + """Test full Flash Attention configuration.""" + from ludic.training.hardware import configure_flash_attention + + # Configure for CUDA device + attn_impl = configure_flash_attention("cuda", disable_flash_attn=False) + print(f"Configured attention: {attn_impl}") + assert attn_impl in ("flash_attention_3", "flash_attention_2", "sdpa") + + # Configure for CPU (should return eager) + attn_impl_cpu = configure_flash_attention("cpu", disable_flash_attn=False) + assert attn_impl_cpu == "eager" + + +@pytest.mark.gpu +def test_model_with_flash_attention(): + """Load a small model with flash attention and run forward pass.""" + from ludic.training.hardware import configure_flash_attention + from transformers import AutoModelForCausalLM, AutoTokenizer + + model_name = "Qwen/Qwen2.5-0.5B-Instruct" + + # Configure flash attention + attn_impl = configure_flash_attention("cuda", disable_flash_attn=False) + print(f"Using attention: {attn_impl}") + + # Load model with flash attention + tokenizer = AutoTokenizer.from_pretrained(model_name) + model = AutoModelForCausalLM.from_pretrained( + model_name, + torch_dtype=torch.bfloat16, + trust_remote_code=True, + attn_implementation=attn_impl, + ).cuda() + + # Verify model loaded with correct attention + print(f"Model attention impl: {model.config._attn_implementation}") + + # Run a forward pass + inputs = tokenizer("Hello, world!", return_tensors="pt").to("cuda") + with torch.no_grad(): + outputs = model(**inputs) + + assert outputs.logits is not None + assert outputs.logits.shape[0] == 1 # batch size + print(f"Forward pass successful, logits shape: {outputs.logits.shape}") diff --git a/tests/test_incomplete_completion.py b/tests/test_incomplete_completion.py index c72989d..d40c044 100644 --- a/tests/test_incomplete_completion.py +++ b/tests/test_incomplete_completion.py @@ -4,7 +4,7 @@ from ludic.agents.base_agent import Agent from ludic.context.full_dialog import FullDialog -from ludic.interaction.single_agent import SingleAgentSyncProtocol +from ludic.interaction.single_agent import SingleAgentProtocol from ludic.parsers import ParseResult from tests._mocks import MockClient, MockEnv @@ -66,7 +66,7 @@ async def test_single_agent_protocol_marks_incomplete_completion_as_parse_error( ctx=FullDialog(), parser=pass_through_parser, ) - protocol = SingleAgentSyncProtocol(agent=agent) + protocol = SingleAgentProtocol(agent=agent) env = MockEnv(max_steps=10, target="1") rollouts = await protocol.run(env=env, max_steps=1) diff --git a/tests/test_interaction.py b/tests/test_interaction.py index 9159c2d..2351710 100644 --- a/tests/test_interaction.py +++ b/tests/test_interaction.py @@ -2,7 +2,7 @@ import pytest from ludic.context.full_dialog import FullDialog -from ludic.interaction.single_agent import SingleAgentSyncProtocol +from ludic.interaction.single_agent import SingleAgentProtocol from ludic.interaction.multi_agent import MultiAgentProtocol from ludic.agents.base_agent import Agent from ludic.inference.client import ChatResponse @@ -28,7 +28,7 @@ async def test_happy_path_terminates_immediately(): env = MockEnv(max_steps=3, target="1") # MockAgent provides a default ctx and a pass-through parser agent = MockAgent(client=MockClient(text="1")) - protocol = SingleAgentSyncProtocol(agent=agent) + protocol = SingleAgentProtocol(agent=agent) # run() now returns List[Rollout] rollouts = await protocol.run( @@ -51,7 +51,7 @@ async def complete(self, request: ChatCompletionRequest, **kwargs): env = MockEnv(max_steps=2, target="1") agent = MockAgent(client=WrongClient()) - protocol = SingleAgentSyncProtocol(agent=agent) + protocol = SingleAgentProtocol(agent=agent) rollouts = await protocol.run( env=env, @@ -97,7 +97,7 @@ async def test_run_episode_uses_action_parser_and_logs_parsed_action(): parser=action_parser ) - protocol = SingleAgentSyncProtocol(agent=agent) + protocol = SingleAgentProtocol(agent=agent) rollouts = await protocol.run( env=env, @@ -312,7 +312,7 @@ async def test_multi_agent_handles_unmanaged_bot_turns(): @pytest.mark.asyncio async def test_single_agent_protocol_logs_parser_failure_without_env_step(): """ - If the agent parser fails, SingleAgentSyncProtocol should: + If the agent parser fails, SingleAgentProtocol should: - NOT call env.step() - log a synthetic step with parse_error info - feed the synthetic observation back to the agent context @@ -337,7 +337,7 @@ def always_fail_parser(_: str) -> ParseResult: ctx=FullDialog(), parser=always_fail_parser, ) - protocol = SingleAgentSyncProtocol(agent=agent) + protocol = SingleAgentProtocol(agent=agent) rollouts = await protocol.run(env=env, max_steps=1) @@ -497,7 +497,7 @@ async def test_single_agent_max_steps_truncation(): # Agent always says "wrong", env wants "correct" env = MockEnv(max_steps=10, target="correct") # env allows many steps agent = MockAgent(client=MockClient(text="wrong")) - protocol = SingleAgentSyncProtocol(agent=agent) + protocol = SingleAgentProtocol(agent=agent) # Protocol max_steps=3, so we'll hit that before env's max_steps rollouts = await protocol.run(env=env, max_steps=3) @@ -533,7 +533,7 @@ async def test_single_agent_env_truncation_preserved(): # Env will truncate after 2 wrong answers env = MockEnv(max_steps=2, target="correct") agent = MockAgent(client=MockClient(text="wrong")) - protocol = SingleAgentSyncProtocol(agent=agent) + protocol = SingleAgentProtocol(agent=agent) # Protocol allows many steps, but env will truncate at 2 rollouts = await protocol.run(env=env, max_steps=100) @@ -559,7 +559,7 @@ async def test_single_agent_normal_termination_not_truncated(): """ env = MockEnv(max_steps=10, target="win") agent = MockAgent(client=MockClient(text="win")) - protocol = SingleAgentSyncProtocol(agent=agent) + protocol = SingleAgentProtocol(agent=agent) rollouts = await protocol.run(env=env, max_steps=100) diff --git a/tests/test_public_api_imports.py b/tests/test_public_api_imports.py index fc5609c..35bbd51 100644 --- a/tests/test_public_api_imports.py +++ b/tests/test_public_api_imports.py @@ -8,7 +8,7 @@ def test_top_level_exports_import() -> None: from ludic.context import ContextStrategy, FullDialog, TruncatedThinkingContext # noqa: F401 from ludic.envs import LudicEnv, SingleAgentEnv, DatasetQAEnv # noqa: F401 from ludic.inference import VLLMChatClient, start_vllm_server, wait_for_vllm_health # noqa: F401 - from ludic.interaction import InteractionProtocol, SingleAgentSyncProtocol, MultiAgentProtocol, TraceCollector # noqa: F401 + from ludic.interaction import InteractionProtocol, SingleAgentProtocol, MultiAgentProtocol, TraceCollector # noqa: F401 from ludic.parsers import boxed_parser, xml_tag_parser, compose_parsers, think_prefix_parser # noqa: F401 from ludic.distributed import create_vllm_publisher # noqa: F401 from ludic.types import Rollout, Step # noqa: F401 diff --git a/tests/test_rollout_engine.py b/tests/test_rollout_engine.py index 20c17ed..85f6010 100644 --- a/tests/test_rollout_engine.py +++ b/tests/test_rollout_engine.py @@ -8,7 +8,7 @@ from ludic.agents.base_agent import Agent from ludic.inference.client import ChatResponse from ludic.interaction.base import InteractionProtocol -from ludic.interaction.single_agent import SingleAgentSyncProtocol +from ludic.interaction.single_agent import SingleAgentProtocol from ludic.context.full_dialog import FullDialog from ludic.envs.env import LudicEnv from ludic.inference.request import ChatCompletionRequest, InferenceSpec, ReturnSpec @@ -111,7 +111,7 @@ async def test_generate_rollouts_basic_metadata_and_termination( mock_agent, ) -> None: protocol_registry: ProtocolRegistry = { - "mock_protocol": lambda: SingleAgentSyncProtocol(agent=mock_agent) + "mock_protocol": lambda: SingleAgentProtocol(agent=mock_agent) } engine = RolloutEngine( @@ -203,7 +203,7 @@ async def test_generate_rollouts_unknown_env_raises( mock_agent, ) -> None: protocol_registry = { - "mock_protocol": lambda: SingleAgentSyncProtocol(agent=mock_agent) + "mock_protocol": lambda: SingleAgentProtocol(agent=mock_agent) } engine = RolloutEngine( env_registry=env_registry, @@ -259,10 +259,10 @@ async def test_generate_rollouts_heterogeneous_protocols( """ # Define two different agent/protocol setups agent_A = MockAgent(client=MockClient(text="Agent A says hi")) - protocol_A = SingleAgentSyncProtocol(agent=agent_A) + protocol_A = SingleAgentProtocol(agent=agent_A) agent_B = MockAgent(client=MockClient(text="Agent B says hi")) - protocol_B = SingleAgentSyncProtocol(agent=agent_B) + protocol_B = SingleAgentProtocol(agent=agent_B) protocol_registry = { "protocol_A": lambda: protocol_A, @@ -317,7 +317,7 @@ async def test_generate_rollouts_writes_jsonl( jsonl_path = tmp_path / "rollouts.jsonl" protocol_registry = { - "mock_protocol": lambda: SingleAgentSyncProtocol(agent=mock_agent) + "mock_protocol": lambda: SingleAgentProtocol(agent=mock_agent) } engine = RolloutEngine( @@ -373,7 +373,7 @@ async def test_generate_batch_uses_model_token_ids_when_available( ) protocol_registry = { - "token_protocol": lambda: SingleAgentSyncProtocol(agent=agent) + "token_protocol": lambda: SingleAgentProtocol(agent=agent) } engine = RolloutEngine( @@ -429,7 +429,7 @@ async def test_generate_batch_raises_if_no_token_ids_and_no_retokenize( mock_agent, ) -> None: protocol_registry = { - "mock_protocol": lambda: SingleAgentSyncProtocol(agent=mock_agent) + "mock_protocol": lambda: SingleAgentProtocol(agent=mock_agent) } engine = RolloutEngine( @@ -471,7 +471,7 @@ async def test_rollout_batch_source_next_batch_integration( parser=_mock_parser, ) protocol_registry = { - "mock_protocol": lambda: SingleAgentSyncProtocol(agent=agent) + "mock_protocol": lambda: SingleAgentProtocol(agent=agent) } engine = RolloutEngine( @@ -527,7 +527,7 @@ async def test_rollout_batch_source_passes_sample_filter( parser=_mock_parser, ) protocol_registry = { - "mock_protocol": lambda: SingleAgentSyncProtocol(agent=agent) + "mock_protocol": lambda: SingleAgentProtocol(agent=agent) } engine = RolloutEngine( @@ -579,7 +579,7 @@ async def test_saw_item_contains_truncation_flags( parser=_mock_parser, ) # Never terminates the env since it never outputs target="win" protocol_registry = { - "mock_protocol": lambda: SingleAgentSyncProtocol(agent=agent), + "mock_protocol": lambda: SingleAgentProtocol(agent=agent), } engine = RolloutEngine( @@ -634,7 +634,7 @@ async def test_generate_batch_applies_sample_filter_and_updates_counts( parser=_mock_parser, ) # Never terminates the env since it never outputs target="win" protocol_registry = { - "mock_protocol": lambda: SingleAgentSyncProtocol(agent=agent), + "mock_protocol": lambda: SingleAgentProtocol(agent=agent), } engine = RolloutEngine( @@ -701,7 +701,7 @@ async def complete( # type: ignore[override] parser=_mock_parser, ) protocol_registry = { - "mock_protocol": lambda: SingleAgentSyncProtocol(agent=agent), + "mock_protocol": lambda: SingleAgentProtocol(agent=agent), } engine = RolloutEngine( diff --git a/uv.lock b/uv.lock index a49fea3..a17cec9 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,11 @@ version = 1 revision = 3 requires-python = "==3.12.*" +resolution-markers = [ + "sys_platform != 'darwin' and sys_platform != 'linux'", + "sys_platform == 'darwin'", + "sys_platform == 'linux'", +] [[package]] name = "accelerate" @@ -8,12 +13,15 @@ version = "1.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, + { name = "numpy", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, { name = "packaging" }, { name = "psutil" }, { name = "pyyaml" }, { name = "safetensors" }, - { name = "torch" }, + { name = "torch", version = "2.9.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.9.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { name = "torch", version = "2.9.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin' and sys_platform != 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4a/8e/ac2a9566747a93f8be36ee08532eb0160558b07630a081a6056a9f89bf1d/accelerate-1.12.0.tar.gz", hash = "sha256:70988c352feb481887077d2ab845125024b2a137a5090d6d7a32b57d03a45df6", size = 398399, upload-time = "2025-11-21T11:27:46.973Z" } wheels = [ @@ -78,11 +86,11 @@ wheels = [ [[package]] name = "annotated-doc" -version = "0.0.3" +version = "0.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/a6/dc46877b911e40c00d395771ea710d5e77b6de7bacd5fdcd78d70cc5a48f/annotated_doc-0.0.3.tar.gz", hash = "sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda", size = 5535, upload-time = "2025-10-24T14:57:10.718Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/b7/cf592cb5de5cb3bade3357f8d2cf42bf103bbe39f459824b4939fd212911/annotated_doc-0.0.3-py3-none-any.whl", hash = "sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580", size = 5488, upload-time = "2025-10-24T14:57:09.462Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, ] [[package]] @@ -99,41 +107,31 @@ name = "anthropic" version = "0.71.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "docstring-parser" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "typing-extensions" }, + { name = "anyio", marker = "sys_platform == 'linux'" }, + { name = "distro", marker = "sys_platform == 'linux'" }, + { name = "docstring-parser", marker = "sys_platform == 'linux'" }, + { name = "httpx", marker = "sys_platform == 'linux'" }, + { name = "jiter", marker = "sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'linux'" }, + { name = "sniffio", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/82/4f/70682b068d897841f43223df82d96ec1d617435a8b759c4a2d901a50158b/anthropic-0.71.0.tar.gz", hash = "sha256:eb8e6fa86d049061b3ef26eb4cbae0174ebbff21affa6de7b3098da857d8de6a", size = 489102, upload-time = "2025-10-16T15:54:40.08Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5d/77/073e8ac488f335aec7001952825275582fb8f433737e90f24eeef9d878f6/anthropic-0.71.0-py3-none-any.whl", hash = "sha256:85c5015fcdbdc728390f11b17642a65a4365d03b12b799b18b6cc57e71fdb327", size = 355035, upload-time = "2025-10-16T15:54:38.238Z" }, ] -[[package]] -name = "antlr4-python3-runtime" -version = "4.13.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/5f/2cdf6f7aca3b20d3f316e9f505292e1f256a32089bd702034c29ebde6242/antlr4_python3_runtime-4.13.2.tar.gz", hash = "sha256:909b647e1d2fc2b70180ac586df3933e38919c85f98ccc656a96cd3f25ef3916", size = 117467, upload-time = "2024-08-03T19:00:12.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/03/a851e84fcbb85214dc637b6378121ef9a0dd61b4c65264675d8a5c9b1ae7/antlr4_python3_runtime-4.13.2-py3-none-any.whl", hash = "sha256:fe3835eb8d33daece0e799090eda89719dbccee7aa39ef94eed3818cafa5a7e8", size = 144462, upload-time = "2024-08-03T19:00:11.134Z" }, -] - [[package]] name = "anyio" -version = "4.11.0" +version = "4.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, ] [[package]] @@ -141,16 +139,14 @@ name = "apache-tvm-ffi" version = "0.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/45/20/8da071821b2142bdeed757d2859dede4817e0b82a96e9a4d8cfbffd49006/apache_tvm_ffi-0.1.6.tar.gz", hash = "sha256:53088126f7fce11823ddf0fb101e968a90298d79fd68829c0a981f25467a574c", size = 2387987, upload-time = "2025-12-16T19:00:33.523Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/de/4ae5dd4d493b1cea755a25d59088895486432c053cff5a3287b75e36ce54/apache_tvm_ffi-0.1.6-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:5f4c0678854dbf3bfaa37795465f570d79c68759896b04b3d31774af0a03bcb8", size = 1779381, upload-time = "2025-12-16T18:59:59.593Z" }, { url = "https://files.pythonhosted.org/packages/2d/40/2e943cbda764c3266a6966a34e582d3f0ac6046ab6aaa756631df9afd7bf/apache_tvm_ffi-0.1.6-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:653f1d4c8ffd6bca5300fd1825a81373a5be82f31dc79353d1c476fa31cf377a", size = 1936756, upload-time = "2025-12-16T19:00:00.844Z" }, { url = "https://files.pythonhosted.org/packages/a3/91/fc43f155b4d4363e61707655c1f4bee75af1d6dd4a76680f4956dd9846fe/apache_tvm_ffi-0.1.6-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6a2cdfa90860a80e3cfb2364ce3b66a559fa5748de8d593a203b2e5992d92bc1", size = 2013641, upload-time = "2025-12-16T19:00:02.479Z" }, { url = "https://files.pythonhosted.org/packages/14/9b/45208f2a9c70a88fd8e65668c0628f3917625d64668800ff55a2390d7fe0/apache_tvm_ffi-0.1.6-cp312-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223ac7ac08b34a6dbabe7085f23939b4aaa70666e72ddad41015659034e095af", size = 1881149, upload-time = "2025-12-16T19:00:03.776Z" }, { url = "https://files.pythonhosted.org/packages/7d/c5/e3ba08379127578bb3417605b61e9cd5e513184a6947ec7f3fac93d16355/apache_tvm_ffi-0.1.6-cp312-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05cedb3ba7600dc9ae35c17b7325d44ecf02c56c3ba1b62668dca8390da7ec28", size = 1992886, upload-time = "2025-12-16T19:00:05.047Z" }, - { url = "https://files.pythonhosted.org/packages/d6/7b/4df1e523ae4bcbfbe65a3e7ef3c8810cb76e9ae44fa9b44c9fac152ecc2b/apache_tvm_ffi-0.1.6-cp312-abi3-win_amd64.whl", hash = "sha256:a6c29ba9dbc6273f4534bfc0e8a52a784f264724eb62df62daedc2b349dabe85", size = 1758454, upload-time = "2025-12-16T19:00:06.498Z" }, ] [[package]] @@ -186,8 +182,6 @@ 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" }, @@ -196,17 +190,15 @@ wheels = [ { 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" }, ] [[package]] name = "cachetools" -version = "6.2.1" +version = "6.2.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201", size = 31325, upload-time = "2025-10-12T14:55:30.139Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280, upload-time = "2025-10-12T14:55:28.382Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" }, ] [[package]] @@ -215,24 +207,20 @@ version = "5.7.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/b8/c0f6a7d46f816cb18b1fda61a2fe648abe16039f1ff93ea720a6e9fb3cee/cbor2-5.7.1.tar.gz", hash = "sha256:7a405a1d7c8230ee9acf240aad48ae947ef584e8af05f169f3c1bde8f01f8b71", size = 102467, upload-time = "2025-10-24T09:23:06.569Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/54/48426472f0c051982c647331441aed09b271a0500356ae0b7054c813d174/cbor2-5.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bd5ca44891c06f6b85d440836c967187dc1d30b15f86f315d55c675d3a841078", size = 69031, upload-time = "2025-10-24T09:22:25.438Z" }, - { url = "https://files.pythonhosted.org/packages/d3/68/1dd58c7706e9752188358223db58c83f3c48e07f728aa84221ffd244652f/cbor2-5.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:537d73ef930ccc1a7b6a2e8d2cbf81407d270deb18e40cda5eb511bd70f71078", size = 68825, upload-time = "2025-10-24T09:22:26.497Z" }, { url = "https://files.pythonhosted.org/packages/09/4e/380562fe9f9995a1875fb5ec26fd041e19d61f4630cb690a98c5195945fc/cbor2-5.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:edbf814dd7763b6eda27a5770199f6ccd55bd78be8f4367092460261bfbf19d0", size = 286222, upload-time = "2025-10-24T09:22:27.546Z" }, { url = "https://files.pythonhosted.org/packages/7c/bb/9eccdc1ea3c4d5c7cdb2e49b9de49534039616be5455ce69bd64c0b2efe2/cbor2-5.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fc81da8c0e09beb42923e455e477b36ff14a03b9ca18a8a2e9b462de9a953e8", size = 285688, upload-time = "2025-10-24T09:22:28.651Z" }, { url = "https://files.pythonhosted.org/packages/59/8c/4696d82f5bd04b3d45d9a64ec037fa242630c134e3218d6c252b4f59b909/cbor2-5.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e4a7d660d428911a3aadb7105e94438d7671ab977356fdf647a91aab751033bd", size = 277063, upload-time = "2025-10-24T09:22:29.775Z" }, { url = "https://files.pythonhosted.org/packages/95/50/6538e44ca970caaad2fa376b81701d073d84bf597aac07a59d0a253b1a7f/cbor2-5.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:228e0af9c0a9ddf6375b6ae010eaa1942a1901d403f134ac9ee6a76a322483f9", size = 278334, upload-time = "2025-10-24T09:22:30.904Z" }, - { url = "https://files.pythonhosted.org/packages/64/a9/156ccd2207fb26b5b61d23728b4dbdc595d1600125aa79683a4a8ddc9313/cbor2-5.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:2d08a6c0d9ed778448e185508d870f4160ba74f59bb17a966abd0d14d0ff4dd3", size = 68404, upload-time = "2025-10-24T09:22:32.108Z" }, - { url = "https://files.pythonhosted.org/packages/4f/49/adc53615e9dd32c4421f6935dfa2235013532c6e6b28ee515bbdd92618be/cbor2-5.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:752506cfe72da0f4014b468b30191470ee8919a64a0772bd3b36a4fccf5fcefc", size = 64047, upload-time = "2025-10-24T09:22:33.147Z" }, { url = "https://files.pythonhosted.org/packages/d5/7d/383bafeabb54c17fe5b6d5aca4e863e6b7df10bcc833b34aa169e9dfce1a/cbor2-5.7.1-py3-none-any.whl", hash = "sha256:68834e4eff2f56629ce6422b0634bc3f74c5a4269de5363f5265fe452c706ba7", size = 23829, upload-time = "2025-10-24T09:23:05.54Z" }, ] [[package]] name = "certifi" -version = "2025.10.5" +version = "2025.11.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, ] [[package]] @@ -240,12 +228,10 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "implementation_name != 'PyPy' and sys_platform == 'linux'" }, ] 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" }, @@ -253,9 +239,6 @@ wheels = [ { 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" }, ] [[package]] @@ -285,14 +268,14 @@ wheels = [ [[package]] name = "click" -version = "8.2.1" +version = "8.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[package]] @@ -318,10 +301,10 @@ name = "compressed-tensors" version = "0.12.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "loguru" }, - { name = "pydantic" }, - { name = "torch" }, - { name = "transformers" }, + { name = "loguru", marker = "sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.9.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, + { name = "transformers", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a2/79/4c5c1cd14266f8cf2650bdb940f986ce7fcaeb56aad8cfa9e9afedf14e2f/compressed_tensors-0.12.2.tar.gz", hash = "sha256:5bb40856dd17f128ab73557ecc73799f80db4dd82fab6de875f1e6899b9ea0c4", size = 190409, upload-time = "2025-10-07T14:30:59.302Z" } wheels = [ @@ -333,11 +316,10 @@ name = "cryptography" version = "46.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'linux'" }, ] 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" }, @@ -349,10 +331,6 @@ wheels = [ { 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/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" }, @@ -364,9 +342,6 @@ wheels = [ { 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]] @@ -374,12 +349,11 @@ name = "cuda-bindings" version = "13.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder" }, + { name = "cuda-pathfinder", marker = "sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/53/3d/c8ed9d169843091f3f0d6b8218e826fd59520a37e0434c204feada597988/cuda_bindings-13.1.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e75ad0cb863330df784236d289612d71ca855c013d19ae00e5693574abd6915", size = 15530160, upload-time = "2025-12-09T22:05:55.386Z" }, { url = "https://files.pythonhosted.org/packages/4a/8e/368295623ee43fba622909d780fbb6863efc1638dff55f67a0f04eac6470/cuda_bindings-13.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25785d1a3cdcd98f151240fd5efd025609319a6720a217dee2a929241749d488", size = 16110386, upload-time = "2025-12-09T22:05:57.71Z" }, - { url = "https://files.pythonhosted.org/packages/60/1f/ecc4701ade3e85f091c625a920574527b9daf7fb354189fbfbc5516af6cd/cuda_bindings-13.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:ccde9c95c0e953b31fe7731bb08da9d0a34b1770498df9a3c156fdfdbe3951ad", size = 15250028, upload-time = "2025-12-09T22:06:00.346Z" }, ] [[package]] @@ -395,8 +369,8 @@ name = "cuda-python" version = "13.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-bindings" }, - { name = "cuda-pathfinder" }, + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-pathfinder", marker = "sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/cd/08/b5e3b9822662d72d540d830531e3ab6a7cabbda3dd56175696aabccfeb76/cuda_python-13.1.1-py3-none-any.whl", hash = "sha256:944cc4fe6482673d28dd545797a28840945a1668739328fa2ad1e9be4f7050d9", size = 8038, upload-time = "2025-12-09T22:13:10.719Z" }, @@ -407,18 +381,17 @@ name = "cupy-cuda12x" version = "13.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "fastrlock" }, - { name = "numpy" }, + { name = "fastrlock", marker = "sys_platform == 'linux'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, ] 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" }, ] [[package]] name = "datasets" -version = "4.4.1" +version = "4.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dill" }, @@ -427,7 +400,8 @@ dependencies = [ { name = "httpx" }, { name = "huggingface-hub" }, { name = "multiprocess" }, - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, + { name = "numpy", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, { name = "packaging" }, { name = "pandas" }, { name = "pyarrow" }, @@ -436,9 +410,9 @@ dependencies = [ { name = "tqdm" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/bf/0dae295d6d1ba0b1a200a9dd216838464b5bbd05da01407cb1330b377445/datasets-4.4.1.tar.gz", hash = "sha256:80322699aa8c0bbbdb7caa87906da689c3c2e29523cff698775c67f28fdab1fc", size = 585341, upload-time = "2025-11-05T16:00:38.162Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/54/9359803da96bc65439a28fbb014dc2c90b7d4d8034a93b72362b0d40191f/datasets-4.4.2.tar.gz", hash = "sha256:9de16e415c4ba4713eac0493f7c7dc74f3aa21599297f00cc6ddab409cb7b24b", size = 586474, upload-time = "2025-12-19T15:03:09.129Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5e/6f8d874366788ad5d549e9ba258037d974dda6e004843be1bda794571701/datasets-4.4.1-py3-none-any.whl", hash = "sha256:c1163de5211e42546079ab355cc0250c7e6db16eb209ac5ac6252f801f596c44", size = 511591, upload-time = "2025-11-05T16:00:36.365Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b5/fefa518c809de7bced5cddb7c21c010da66fa2ae494bda96844a280cc6ce/datasets-4.4.2-py3-none-any.whl", hash = "sha256:6f5ef3417504d9cd663c71c1b90b9a494ff4c2076a2cd6a6e40ceee6ad95befc", size = 512268, upload-time = "2025-12-19T15:03:07.087Z" }, ] [[package]] @@ -446,8 +420,8 @@ name = "depyf" version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astor" }, - { name = "dill" }, + { name = "astor", marker = "sys_platform == 'linux'" }, + { name = "dill", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/88/35/83fb0178212279aa0af031031905804c6de5618435d229f41ed21bb9ad2c/depyf-0.20.0.tar.gz", hash = "sha256:fb7683bd72c44f67b56029df2c47721e9a02ffa4d7b19095f1c54c4ebf797a98", size = 6168761, upload-time = "2025-10-13T12:33:38.589Z" } wheels = [ @@ -490,6 +464,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, +] + [[package]] name = "docstring-parser" version = "0.17.0" @@ -513,8 +501,8 @@ name = "email-validator" version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "dnspython" }, - { name = "idna" }, + { name = "dnspython", marker = "sys_platform == 'linux'" }, + { name = "idna", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } wheels = [ @@ -523,65 +511,86 @@ wheels = [ [[package]] name = "fastapi" -version = "0.121.1" +version = "0.128.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-doc" }, - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, + { name = "annotated-doc", marker = "sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'linux'" }, + { name = "starlette", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/a4/29e1b861fc9017488ed02ff1052feffa40940cb355ed632a8845df84ce84/fastapi-0.121.1.tar.gz", hash = "sha256:b6dba0538fd15dab6fe4d3e5493c3957d8a9e1e9257f56446b5859af66f32441", size = 342523, upload-time = "2025-11-08T21:48:14.068Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/fd/2e6f7d706899cc08690c5f6641e2ffbfffe019e8f16ce77104caa5730910/fastapi-0.121.1-py3-none-any.whl", hash = "sha256:2c5c7028bc3a58d8f5f09aecd3fd88a000ccc0c5ad627693264181a3c33aa1fc", size = 109192, upload-time = "2025-11-08T21:48:12.458Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, ] [package.optional-dependencies] standard = [ - { name = "email-validator" }, - { name = "fastapi-cli", extra = ["standard"] }, - { name = "httpx" }, - { name = "jinja2" }, - { name = "python-multipart" }, - { name = "uvicorn", extra = ["standard"] }, + { name = "email-validator", marker = "sys_platform == 'linux'" }, + { name = "fastapi-cli", extra = ["standard"], marker = "sys_platform == 'linux'" }, + { name = "httpx", marker = "sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'linux'" }, + { name = "pydantic-extra-types", marker = "sys_platform == 'linux'" }, + { name = "pydantic-settings", marker = "sys_platform == 'linux'" }, + { name = "python-multipart", marker = "sys_platform == 'linux'" }, + { name = "uvicorn", extra = ["standard"], marker = "sys_platform == 'linux'" }, ] [[package]] name = "fastapi-cli" -version = "0.0.14" +version = "0.0.20" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "rich-toolkit" }, - { name = "typer" }, - { name = "uvicorn", extra = ["standard"] }, + { name = "rich-toolkit", marker = "sys_platform == 'linux'" }, + { name = "typer", marker = "sys_platform == 'linux'" }, + { name = "uvicorn", extra = ["standard"], marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/13/11e43d630be84e51ba5510a6da6a11eb93b44b72caa796137c5dddda937b/fastapi_cli-0.0.14.tar.gz", hash = "sha256:ddfb5de0a67f77a8b3271af1460489bd4d7f4add73d11fbfac613827b0275274", size = 17994, upload-time = "2025-10-20T16:33:21.054Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/ca/d90fb3bfbcbd6e56c77afd9d114dd6ce8955d8bb90094399d1c70e659e40/fastapi_cli-0.0.20.tar.gz", hash = "sha256:d17c2634f7b96b6b560bc16b0035ed047d523c912011395f49f00a421692bc3a", size = 19786, upload-time = "2025-12-22T17:13:33.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/e8/bc8bbfd93dcc8e347ce98a3e654fb0d2e5f2739afb46b98f41a30c339269/fastapi_cli-0.0.14-py3-none-any.whl", hash = "sha256:e66b9ad499ee77a4e6007545cde6de1459b7f21df199d7f29aad2adaab168eca", size = 11151, upload-time = "2025-10-20T16:33:19.318Z" }, + { url = "https://files.pythonhosted.org/packages/08/89/5c4eef60524d0fd704eb0706885b82cd5623a43396b94e4a5b17d3a3f516/fastapi_cli-0.0.20-py3-none-any.whl", hash = "sha256:e58b6a0038c0b1532b7a0af690656093dee666201b6b19d3c87175b358e9f783", size = 12390, upload-time = "2025-12-22T17:13:31.708Z" }, ] [package.optional-dependencies] standard = [ - { name = "fastapi-cloud-cli" }, - { name = "uvicorn", extra = ["standard"] }, + { name = "fastapi-cloud-cli", marker = "sys_platform == 'linux'" }, + { name = "uvicorn", extra = ["standard"], marker = "sys_platform == 'linux'" }, ] [[package]] name = "fastapi-cloud-cli" -version = "0.3.1" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "httpx" }, - { name = "pydantic", extra = ["email"] }, - { name = "rich-toolkit" }, - { name = "rignore" }, - { name = "sentry-sdk" }, - { name = "typer" }, - { name = "uvicorn", extra = ["standard"] }, + { name = "fastar", marker = "sys_platform == 'linux'" }, + { name = "httpx", marker = "sys_platform == 'linux'" }, + { name = "pydantic", extra = ["email"], marker = "sys_platform == 'linux'" }, + { name = "rich-toolkit", marker = "sys_platform == 'linux'" }, + { name = "rignore", marker = "sys_platform == 'linux'" }, + { name = "sentry-sdk", marker = "sys_platform == 'linux'" }, + { name = "typer", marker = "sys_platform == 'linux'" }, + { name = "uvicorn", extra = ["standard"], marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/48/0f14d8555b750dc8c04382804e4214f1d7f55298127f3a0237ba566e69dd/fastapi_cloud_cli-0.3.1.tar.gz", hash = "sha256:8c7226c36e92e92d0c89827e8f56dbf164ab2de4444bd33aa26b6c3f7675db69", size = 24080, upload-time = "2025-10-09T11:32:58.174Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/5d/3b33438de35521fab4968b232caa9a4bd568a5078f2b2dfb7bb8a4528603/fastapi_cloud_cli-0.8.0.tar.gz", hash = "sha256:cf07c502528bfd9e6b184776659f05d9212811d76bbec9fbb6bf34bed4c7456f", size = 30257, upload-time = "2025-12-23T12:08:33.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/8e/abb95ef59e91bb5adaa2d18fbf9ea70fd524010bb03f406a2dd2a4775ef9/fastapi_cloud_cli-0.8.0-py3-none-any.whl", hash = "sha256:e9f40bee671d985fd25d7a5409b56d4f103777bf8a0c6d746ea5fbf97a8186d9", size = 22306, upload-time = "2025-12-23T12:08:32.68Z" }, +] + +[[package]] +name = "fastar" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/e7/f89d54fb04104114dd0552836dc2b47914f416cc0e200b409dd04a33de5e/fastar-0.8.0.tar.gz", hash = "sha256:f4d4d68dbf1c4c2808f0e730fac5843493fc849f70fe3ad3af60dfbaf68b9a12", size = 68524, upload-time = "2025-11-26T02:36:00.72Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/79/7f5a5e5513e6a737e5fb089d9c59c74d4d24dc24d581d3aa519b326bedda/fastapi_cloud_cli-0.3.1-py3-none-any.whl", hash = "sha256:7d1a98a77791a9d0757886b2ffbf11bcc6b3be93210dd15064be10b216bf7e00", size = 19711, upload-time = "2025-10-09T11:32:57.118Z" }, + { url = "https://files.pythonhosted.org/packages/22/7e/1ae005addc789924a9268da2394d3bb5c6f96836f7e37b7e3d23c2362675/fastar-0.8.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9d210da2de733ca801de83e931012349d209f38b92d9630ccaa94bd445bdc9b8", size = 868938, upload-time = "2025-11-26T02:33:51.119Z" }, + { url = "https://files.pythonhosted.org/packages/a6/77/290a892b073b84bf82e6b2259708dfe79c54f356e252c2dd40180b16fe07/fastar-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa02270721517078a5bd61a38719070ac2537a4aa6b6c48cf369cf2abc59174a", size = 765204, upload-time = "2025-11-26T02:32:47.02Z" }, + { url = "https://files.pythonhosted.org/packages/d0/00/c3155171b976003af3281f5258189f1935b15d1221bfc7467b478c631216/fastar-0.8.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83c391e5b789a720e4d0029b9559f5d6dee3226693c5b39c0eab8eaece997e0f", size = 764717, upload-time = "2025-11-26T02:33:02.453Z" }, + { url = "https://files.pythonhosted.org/packages/b7/43/405b7ad76207b2c11b7b59335b70eac19e4a2653977f5588a1ac8fed54f4/fastar-0.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3258d7a78a72793cdd081545da61cabe85b1f37634a1d0b97ffee0ff11d105ef", size = 931502, upload-time = "2025-11-26T02:33:18.619Z" }, + { url = "https://files.pythonhosted.org/packages/da/8a/a3dde6d37cc3da4453f2845cdf16675b5686b73b164f37e2cc579b057c2c/fastar-0.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6eab95dd985cdb6a50666cbeb9e4814676e59cfe52039c880b69d67cfd44767", size = 821454, upload-time = "2025-11-26T02:33:33.427Z" }, + { url = "https://files.pythonhosted.org/packages/da/c1/904fe2468609c8990dce9fe654df3fbc7324a8d8e80d8240ae2c89757064/fastar-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:829b1854166141860887273c116c94e31357213fa8e9fe8baeb18bd6c38aa8d9", size = 821647, upload-time = "2025-11-26T02:34:07Z" }, + { url = "https://files.pythonhosted.org/packages/c8/73/a0642ab7a400bc07528091785e868ace598fde06fcd139b8f865ec1b6f3c/fastar-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1667eae13f9457a3c737f4376d68e8c3e548353538b28f7e4273a30cb3965cd", size = 986342, upload-time = "2025-11-26T02:34:53.371Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/60c1bfa6edab72366461a95f053d0f5f7ab1825fe65ca2ca367432cd8629/fastar-0.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b864a95229a7db0814cd9ef7987cb713fd43dce1b0d809dd17d9cd6f02fdde3e", size = 1040207, upload-time = "2025-11-26T02:35:10.65Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a0/0d624290dec622e7fa084b6881f456809f68777d54a314f5dde932714506/fastar-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c05fbc5618ce17675a42576fa49858d79734627f0a0c74c0875ab45ee8de340c", size = 1045031, upload-time = "2025-11-26T02:35:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/a7/74/cf663af53c4706ba88e6b4af44a6b0c3bd7d7ca09f079dc40647a8f06585/fastar-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7f41c51ee96f338662ee3c3df4840511ba3f9969606840f1b10b7cb633a3c716", size = 994877, upload-time = "2025-11-26T02:35:45.797Z" }, ] [[package]] @@ -594,36 +603,45 @@ wheels = [ { 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" }, ] [[package]] name = "filelock" -version = "3.20.0" +version = "3.20.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, +] + +[[package]] +name = "flash-attn" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "einops", marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.9.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/3b/b2/8d76c41ad7974ee264754709c22963447f7f8134613fd9ce80984ed0dab7/flash_attn-2.8.3.tar.gz", hash = "sha256:1e71dd64a9e0280e0447b8a0c2541bad4bf6ac65bdeaa2f90e51a9e57de0370d", size = 8447812, upload-time = "2025-08-15T08:28:12.911Z" } [[package]] name = "flashinfer-python" version = "0.5.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "apache-tvm-ffi" }, - { name = "click" }, - { name = "einops" }, - { name = "ninja" }, - { name = "numpy" }, - { name = "nvidia-cudnn-frontend" }, - { name = "nvidia-cutlass-dsl" }, - { name = "nvidia-ml-py" }, - { name = "packaging" }, - { name = "requests" }, - { name = "tabulate" }, - { name = "torch" }, - { name = "tqdm" }, + { name = "apache-tvm-ffi", marker = "sys_platform == 'linux'" }, + { name = "click", marker = "sys_platform == 'linux'" }, + { name = "einops", marker = "sys_platform == 'linux'" }, + { name = "ninja", marker = "sys_platform == 'linux'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, + { name = "nvidia-cudnn-frontend", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cutlass-dsl", marker = "sys_platform == 'linux'" }, + { name = "nvidia-ml-py", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'linux'" }, + { name = "tabulate", marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.9.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, + { name = "tqdm", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/91/cca69baeff24bb3efd12c7479a026432c8717ee47193694010494c528b22/flashinfer_python-0.5.3.tar.gz", hash = "sha256:100d59b0ede47878d2808cd3a1b9039d7a952d66338bc9f68dac192ae1b2e3f1", size = 4682367, upload-time = "2025-11-20T21:22:46.976Z" } wheels = [ @@ -674,15 +692,39 @@ name = "gguf" version = "0.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "pyyaml" }, - { name = "tqdm" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'linux'" }, + { name = "tqdm", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/08/7de1ca4b71e7bf33b547f82bb22505e221b5fa42f67d635e200e0ad22ad6/gguf-0.17.1.tar.gz", hash = "sha256:36ad71aad900a3e75fc94ebe96ea6029f03a4e44be7627ef7ad3d03e8c7bcb53", size = 89338, upload-time = "2025-06-19T14:00:33.705Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fc/31/6a93a887617ee7deeaa602ca3d02d1c12a6cb8a742a695de5d128f5fa46a/gguf-0.17.1-py3-none-any.whl", hash = "sha256:7bc5aa7eeb1931f7d39b48fdc5b38fda6b294b9dca75cf607ac69557840a3943", size = 96224, upload-time = "2025-06-19T14:00:32.88Z" }, ] +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -726,13 +768,10 @@ version = "0.7.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, - { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, ] [[package]] @@ -793,17 +832,12 @@ version = "3.4.0.post0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2d/30/7ab4b9e88e7946f6beef419f74edcc541df3ea562c7882257b4eaa82417d/ijson-3.4.0.post0.tar.gz", hash = "sha256:9aa02dc70bb245670a6ca7fba737b992aeeb4895360980622f7e568dbf23e41e", size = 67216, upload-time = "2025-10-10T05:29:25.62Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/fe/3b6af0025288e769dbfa30485dae1b3bd3f33f00390f3ee532cbb1c33e9b/ijson-3.4.0.post0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b607a500fca26101be47d2baf7cddb457b819ab60a75ce51ed1092a40da8b2f9", size = 87847, upload-time = "2025-10-10T05:28:07.229Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a5/95ee2ca82f3b1a57892452f6e5087607d56c620beb8ce625475194568698/ijson-3.4.0.post0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4827d9874a6a81625412c59f7ca979a84d01f7f6bfb3c6d4dc4c46d0382b14e0", size = 59815, upload-time = "2025-10-10T05:28:08.448Z" }, - { url = "https://files.pythonhosted.org/packages/51/8d/5a704ab3c17c55c21c86423458db8610626ca99cc9086a74dfeb7ee9054c/ijson-3.4.0.post0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4d4afec780881edb2a0d2dd40b1cdbe246e630022d5192f266172a0307986a7", size = 59648, upload-time = "2025-10-10T05:28:09.307Z" }, { url = "https://files.pythonhosted.org/packages/25/56/ca5d6ca145d007f30b44e747f3c163bc08710ce004af0deaad4a2301339b/ijson-3.4.0.post0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432fb60ffb952926f9438e0539011e2dfcd108f8426ee826ccc6173308c3ff2c", size = 138279, upload-time = "2025-10-10T05:28:10.489Z" }, { url = "https://files.pythonhosted.org/packages/c3/d3/22e3cc806fcdda7ad4c8482ed74db7a017d4a1d49b4300c7bc07052fb561/ijson-3.4.0.post0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54a0e3e05d9a0c95ecba73d9579f146cf6d5c5874116c849dba2d39a5f30380e", size = 149110, upload-time = "2025-10-10T05:28:12.263Z" }, { url = "https://files.pythonhosted.org/packages/3e/04/efb30f413648b9267f5a33920ac124d7ebef3bc4063af8f6ffc8ca11ddcb/ijson-3.4.0.post0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05807edc0bcbd222dc6ea32a2b897f0c81dc7f12c8580148bc82f6d7f5e7ec7b", size = 149026, upload-time = "2025-10-10T05:28:13.557Z" }, { url = "https://files.pythonhosted.org/packages/2d/cf/481165f7046ade32488719300a3994a437020bc41cfbb54334356348f513/ijson-3.4.0.post0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5269af16f715855d9864937f9dd5c348ca1ac49cee6a2c7a1b7091c159e874f", size = 150012, upload-time = "2025-10-10T05:28:14.859Z" }, { url = "https://files.pythonhosted.org/packages/0f/24/642e3289917ecf860386e26dfde775f9962d26ab7f6c2e364ed3ca3c25d8/ijson-3.4.0.post0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b200df83c901f5bfa416d069ac71077aa1608f854a4c50df1b84ced560e9c9ec", size = 142193, upload-time = "2025-10-10T05:28:16.131Z" }, { url = "https://files.pythonhosted.org/packages/0f/f5/fd2f038abe95e553e1c3ee207cda19db9196eb416e63c7c89699a8cf0db7/ijson-3.4.0.post0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6458bd8e679cdff459a0a5e555b107c3bbacb1f382da3fe0f40e392871eb518d", size = 150904, upload-time = "2025-10-10T05:28:17.401Z" }, - { url = "https://files.pythonhosted.org/packages/49/35/24259d22519987928164e6cb8fe3486e1df0899b2999ada4b0498639b463/ijson-3.4.0.post0-cp312-cp312-win32.whl", hash = "sha256:55f7f656b5986326c978cbb3a9eea9e33f3ef6ecc4535b38f1d452c731da39ab", size = 52358, upload-time = "2025-10-10T05:28:18.315Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2b/6f7ade27a8ff5758fc41006dadd2de01730def84fe3e60553b329c59e0d4/ijson-3.4.0.post0-cp312-cp312-win_amd64.whl", hash = "sha256:e15833dcf6f6d188fdc624a31cd0520c3ba21b6855dc304bc7c1a8aeca02d4ac", size = 54789, upload-time = "2025-10-10T05:28:19.552Z" }, ] [[package]] @@ -887,10 +921,10 @@ name = "jsonschema" version = "4.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, + { name = "attrs", marker = "sys_platform == 'linux'" }, + { name = "jsonschema-specifications", marker = "sys_platform == 'linux'" }, + { name = "referencing", marker = "sys_platform == 'linux'" }, + { name = "rpds-py", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } wheels = [ @@ -902,7 +936,7 @@ name = "jsonschema-specifications" version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "referencing" }, + { name = "referencing", marker = "sys_platform == 'linux'" }, ] 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 = [ @@ -918,30 +952,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036, upload-time = "2024-08-13T19:48:58.603Z" }, ] -[[package]] -name = "latex2sympy2-extended" -version = "1.10.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "antlr4-python3-runtime" }, - { name = "sympy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f4/de/472f9115c14c6f6d8a5889cabe3418283d708bde62ce00402c29441deed4/latex2sympy2_extended-1.10.2.tar.gz", hash = "sha256:41a517ffcc5a140e910a7d1646ce6ff440817e5f9d48fc8279d88bd0925bc389", size = 206188, upload-time = "2025-07-02T15:26:06.225Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/60/dfbbf40e3a371388c0e03ff65b01319b7d4023e883df6d7261125772ffdc/latex2sympy2_extended-1.10.2-py3-none-any.whl", hash = "sha256:f910442c5b02a466c1046f47d05cc5285181068b882399281f30102715337fb7", size = 207855, upload-time = "2025-07-02T15:26:04.88Z" }, -] - [[package]] name = "llguidance" version = "1.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/95/48/3f7a9d3ff1b36bba92b5107a3a21286821227afe9ea464736133994d61fb/llguidance-1.3.0.tar.gz", hash = "sha256:861249afd51dc325646834462ea827e57a5c2b2042e108e6aae7059fdad9104d", size = 1070460, upload-time = "2025-10-20T19:58:44.164Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/33/be5acb85cd8cdc4afde33d9c234eece9f318e087920255af3c05864cd3e7/llguidance-1.3.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f7685222660a762e481ac633d49cc559c64980fe2ee59c8f932a5bb5cbc0c2c2", size = 3220647, upload-time = "2025-10-20T19:58:42.542Z" }, - { url = "https://files.pythonhosted.org/packages/82/e6/b48bda5b15efeaeb62bd0dba8fc6a01d4ae5457a85dbb5d18632385fe15c/llguidance-1.3.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:098030ff0687261a3f1bd54cf21fe951fc861d56d37a0671250dd36677eaf224", size = 3099830, upload-time = "2025-10-20T19:58:40.826Z" }, { url = "https://files.pythonhosted.org/packages/aa/11/44389d3d1526d7a5c38ffd587a5ebc61d7bee443ac1dea95f2089ad58f5f/llguidance-1.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f6caca5d78db7f76e1fbb0fff8607b861c32d47fa3d5dee2fc49de27ee269df", size = 2835242, upload-time = "2025-10-20T19:58:34.518Z" }, { url = "https://files.pythonhosted.org/packages/83/a8/1ff2bedb8f9acb46a2d2d603415d272bb622c142ea86f5b95445cc6e366c/llguidance-1.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc17e9dd602c3879bf91664a64bf72f54c74dbfbeb24ccfab6a5fe435b12f7aa", size = 3033133, upload-time = "2025-10-20T19:58:38.721Z" }, - { url = "https://files.pythonhosted.org/packages/5a/7e/809349638231f469b9056c0e1bfd924d5ef5558b3b3ec72d093b6fad33b1/llguidance-1.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:1d1cd1c8618d1a13605d3e057c978651e551c8c469b481ee4041f1d6c436002d", size = 2789946, upload-time = "2025-10-20T19:58:45.958Z" }, ] [[package]] @@ -950,11 +968,8 @@ 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" }, ] [[package]] @@ -962,10 +977,10 @@ name = "lm-format-enforcer" version = "0.11.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "interegular" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "pyyaml" }, + { name = "interegular", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/84/d5/41cd417ba7dfdbbcfe46cebf81fb3dfd7c591b89897560ad05bb410a465d/lm_format_enforcer-0.11.3.tar.gz", hash = "sha256:e68081c108719cce284a9bcc889709b26ffb085a1945b5eba3a12cfa96d528da", size = 40258, upload-time = "2025-08-24T19:37:47.527Z" } wheels = [ @@ -976,10 +991,6 @@ wheels = [ name = "loguru" version = "0.7.3" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "win32-setctime", marker = "sys_platform == 'win32'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, @@ -992,21 +1003,24 @@ source = { editable = "." } dependencies = [ { name = "aiohttp" }, { name = "beartype" }, + { name = "datasets" }, + { name = "flash-attn", marker = "sys_platform == 'linux'" }, { name = "jaxtyping" }, { name = "openai" }, { name = "peft" }, { name = "rich" }, - { name = "torch" }, - { name = "vllm" }, + { name = "setuptools" }, + { name = "torch", version = "2.9.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.9.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { name = "torch", version = "2.9.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin' and sys_platform != 'linux'" }, + { name = "torch-c-dlpack-ext" }, + { name = "vllm", marker = "sys_platform == 'linux'" }, + { name = "wandb" }, ] [package.optional-dependencies] -examples = [ - { name = "datasets" }, - { name = "math-verify" }, -] -pipelinerl = [ - { name = "redis" }, +code-exec = [ + { name = "docker" }, ] [package.dev-dependencies] @@ -1031,17 +1045,21 @@ typing = [ requires-dist = [ { name = "aiohttp", specifier = ">=3.13.2" }, { name = "beartype", specifier = ">=0.22.9" }, - { name = "datasets", marker = "extra == 'examples'", specifier = "==4.4.1" }, + { name = "datasets", specifier = ">=4.4.2" }, + { name = "docker", marker = "extra == 'code-exec'", specifier = ">=7.1.0" }, + { name = "flash-attn", marker = "sys_platform == 'linux'", specifier = ">=2.7.0" }, { name = "jaxtyping", specifier = ">=0.3.4" }, - { name = "math-verify", marker = "extra == 'examples'", specifier = "==0.8.0" }, { name = "openai", specifier = ">=2.7.1" }, { name = "peft", specifier = ">=0.18.0" }, - { name = "redis", marker = "extra == 'pipelinerl'", specifier = ">=7.1.0" }, { name = "rich", specifier = ">=14.2.0" }, - { name = "torch", specifier = ">=2.8.0" }, - { name = "vllm", specifier = ">=0.13.0" }, + { name = "setuptools", specifier = ">=79.0.1" }, + { name = "torch", marker = "sys_platform != 'linux'", specifier = ">=2.9.0", index = "https://download.pytorch.org/whl/cpu" }, + { name = "torch", marker = "sys_platform == 'linux'", specifier = ">=2.9.0", index = "https://download.pytorch.org/whl/cu128" }, + { name = "torch-c-dlpack-ext", specifier = ">=0.1.4" }, + { name = "vllm", marker = "sys_platform == 'linux'", specifier = ">=0.12.0" }, + { name = "wandb", specifier = ">=0.23.1" }, ] -provides-extras = ["pipelinerl", "examples"] +provides-extras = ["code-exec"] [package.metadata.requires-dev] dev = [ @@ -1088,37 +1106,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, ] -[[package]] -name = "math-verify" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "latex2sympy2-extended" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/35/b5/b1db6fa6b6c28ebbe1889ee11a4703a72a2ca7750ec415f4559c758cf01a/math_verify-0.8.0.tar.gz", hash = "sha256:3295e0adb94bfe553ff6e3189c44f1916a85aa24ab5d1900f2086a706e28f7c4", size = 60191, upload-time = "2025-07-02T15:52:07.209Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/9f/59979f699b5c97334298f1295bc9fcdc9904d98d2276479bffff863d23b1/math_verify-0.8.0-py3-none-any.whl", hash = "sha256:31ca651296d817a9bb3fd58ca1fd0d192dcea709b1e5ecf2d0a4514c16f89087", size = 29994, upload-time = "2025-07-02T15:52:05.023Z" }, -] - [[package]] name = "mcp" version = "1.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "jsonschema" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, + { name = "anyio", marker = "sys_platform == 'linux'" }, + { name = "httpx", marker = "sys_platform == 'linux'" }, + { name = "httpx-sse", marker = "sys_platform == 'linux'" }, + { name = "jsonschema", marker = "sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'linux'" }, + { name = "pydantic-settings", marker = "sys_platform == 'linux'" }, + { name = "pyjwt", extra = ["crypto"], marker = "sys_platform == 'linux'" }, + { name = "python-multipart", marker = "sys_platform == 'linux'" }, + { name = "sse-starlette", marker = "sys_platform == 'linux'" }, + { name = "starlette", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, + { name = "typing-inspection", marker = "sys_platform == 'linux'" }, + { name = "uvicorn", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387, upload-time = "2025-12-19T10:19:56.985Z" } wheels = [ @@ -1136,66 +1141,26 @@ wheels = [ [[package]] name = "mistral-common" -version = "1.8.5" +version = "1.8.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jsonschema" }, - { name = "numpy" }, - { name = "pillow" }, - { name = "pydantic" }, - { name = "pydantic-extra-types", extra = ["pycountry"] }, - { name = "requests" }, - { name = "tiktoken" }, - { name = "typing-extensions" }, + { name = "jsonschema", marker = "sys_platform == 'linux'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, + { name = "pillow", marker = "sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'linux'" }, + { name = "pydantic-extra-types", extra = ["pycountry"], marker = "sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'linux'" }, + { name = "tiktoken", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/ff/1992a00ccc936f2c6e69ecb1f2cac678e0fd46c53c71bdab99eda4f89dfd/mistral_common-1.8.5.tar.gz", hash = "sha256:9f6204ede9c807f09040a208a9381ae78ef93e2e5a9cd5202dc12e712a025de8", size = 6331923, upload-time = "2025-09-12T06:43:01.937Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/bb/6fc2e46d9920c80f0d053d58be5b0546c18010ff3a5f9b9d91299226e989/mistral_common-1.8.8.tar.gz", hash = "sha256:8ae28b3f88bce1b9396f5d1107e5ea87e4130486b9f6d811df6d5ac07bff2186", size = 6337014, upload-time = "2025-12-22T10:51:47.245Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/4a/54e19c5e75939fd9418c7b806c21d12cf252ea2ba38f122b597272b459dd/mistral_common-1.8.5-py3-none-any.whl", hash = "sha256:f3cf87b61958a00485e603f3fe0530eb509d7e9b2f7178329dcd260e307eced1", size = 6515140, upload-time = "2025-09-12T06:42:59.622Z" }, + { url = "https://files.pythonhosted.org/packages/73/02/c1866598c8e94a4d0593b73e6dec0afea722227b9b3223bf6bb8ab269fa7/mistral_common-1.8.8-py3-none-any.whl", hash = "sha256:f63ce79b1867b3fc7c8b66fcaedab3b07966185567558038dc02321c17e4f39f", size = 6518005, upload-time = "2025-12-22T10:51:44.88Z" }, ] [package.optional-dependencies] image = [ - { name = "opencv-python-headless" }, -] - -[[package]] -name = "mlx" -version = "0.29.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mlx-metal", marker = "sys_platform == 'darwin'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/f5/14e12e219a2715296150d35f930dc3a6ff319cd60126408e563f03100113/mlx-0.29.3-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:86c62791ce930028d75c41b88b4e3ceb58f5f2e263ff9bfacda998b0c03d9544", size = 549516, upload-time = "2025-10-17T19:18:13.831Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e2/5177c80e8c33a8be89fa45fa0a839d5b6a5578687d0ec973bf03638a4e73/mlx-0.29.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cddf6bcdc561094af6b3f0706f8768ecc5216a97eb6973e838c3ac2e2fca2cc8", size = 549509, upload-time = "2025-10-17T19:17:21.517Z" }, - { url = "https://files.pythonhosted.org/packages/11/89/aa424217a7a0291b84f8969d504ac63f5af0ef60f248fe5562c3d6e44048/mlx-0.29.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:b2e1a249437d017a7425358420d28e641b7bc9c2650f3e013c1b1f4f239d8533", size = 549511, upload-time = "2025-10-17T19:16:54.227Z" }, -] - -[[package]] -name = "mlx-lm" -version = "0.28.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "mlx", marker = "sys_platform == 'darwin'" }, - { name = "numpy" }, - { name = "protobuf" }, - { name = "pyyaml" }, - { name = "transformers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/51/f6/15e002d52c28d8c544ec3aaf9053677468333e6ef0e76ea68579fd77b76d/mlx_lm-0.28.3.tar.gz", hash = "sha256:75df2b925d343ebaf50b63008dede4fe98cd3b02b1b24b7da71ebeb198d674f0", size = 214455, upload-time = "2025-10-17T21:44:33.921Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/a6/db3b44a5ac1a1174605628b0a477fbe4632d4fad1f94cf08647e27cc79ad/mlx_lm-0.28.3-py3-none-any.whl", hash = "sha256:ec103e2c9a06bd2cbafd41aafc975e40262176f7360d4f53ec342cebb9e0e6ea", size = 294506, upload-time = "2025-10-17T21:44:32.447Z" }, -] - -[[package]] -name = "mlx-metal" -version = "0.29.3" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/95/a00054a006df82bb1b5b8f666ae44a676b259146fadbff90fe654309fefc/mlx_metal-0.29.3-py3-none-macosx_13_0_arm64.whl", hash = "sha256:27b5a4d905202a71e84d9fd559ea0236813f6f960ef494e5cafe9c45df4c9d7c", size = 36817352, upload-time = "2025-10-17T19:19:25.801Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d8/5ee91eac16dfcf0334103120b47d4abd8c890ccc0d73d3eee4770ce8810f/mlx_metal-0.29.3-py3-none-macosx_14_0_arm64.whl", hash = "sha256:f426d4b67f96b4d6f0ed50d5992933595aadb370dc3e9ed2410bafbc16229882", size = 36555573, upload-time = "2025-10-17T19:18:42.098Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9a/39b7ecdf21cf2a39ced8d7933eed65c6cb38295cadfd0907dd1abd4d1ded/mlx_metal-0.29.3-py3-none-macosx_15_0_arm64.whl", hash = "sha256:106616f7f825851043c53d3dc186965c003985da9cbb6e5c034f35108fc1fc27", size = 36549163, upload-time = "2025-10-17T19:18:37.701Z" }, + { name = "opencv-python-headless", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -1203,13 +1168,13 @@ name = "model-hosting-container-standards" version = "0.1.12" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "fastapi" }, - { name = "httpx" }, - { name = "jmespath" }, - { name = "pydantic" }, - { name = "setuptools" }, - { name = "starlette" }, - { name = "supervisor" }, + { name = "fastapi", marker = "sys_platform == 'linux'" }, + { name = "httpx", marker = "sys_platform == 'linux'" }, + { name = "jmespath", marker = "sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'linux'" }, + { name = "setuptools", marker = "sys_platform == 'linux'" }, + { name = "starlette", marker = "sys_platform == 'linux'" }, + { name = "supervisor", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/23/cc/014bdcc700f1d4393578b55df09c1ed76b57feb9a542208d8c25e7c0bb1b/model_hosting_container_standards-0.1.12.tar.gz", hash = "sha256:5a38814201d319eaf258d816697caa16d39b5222319c2d5116d779b30babe602", size = 79119, upload-time = "2025-12-15T23:02:58.848Z" } wheels = [ @@ -1231,30 +1196,22 @@ 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/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" }, ] [[package]] name = "msgspec" -version = "0.19.0" +version = "0.20.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" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/9c/bfbd12955a49180cbd234c5d29ec6f74fe641698f0cd9df154a854fc8a15/msgspec-0.20.0.tar.gz", hash = "sha256:692349e588fde322875f8d3025ac01689fead5901e7fb18d6870a44519d62a29", size = 317862, upload-time = "2025-11-24T03:56:28.934Z" } 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/49/d6/9709ee093b7742362c2934bfb1bbe791a1e09bed3ea5d8a18ce552fbfd73/msgspec-0.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:558ed73315efa51b1538fa8f1d3b22c8c5ff6d9a2a62eff87d25829b94fc5054", size = 218852, upload-time = "2025-11-24T03:55:35.575Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a2/488517a43ccf5a4b6b6eca6dd4ede0bd82b043d1539dd6bb908a19f8efd3/msgspec-0.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:509ac1362a1d53aa66798c9b9fd76872d7faa30fcf89b2fba3bcbfd559d56eb0", size = 224937, upload-time = "2025-11-24T03:55:36.859Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/49b832808aa23b85d4f090d1d2e48a4e3834871415031ed7c5fe48723156/msgspec-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1353c2c93423602e7dea1aa4c92f3391fdfc25ff40e0bacf81d34dbc68adb870", size = 222858, upload-time = "2025-11-24T03:55:38.187Z" }, + { url = "https://files.pythonhosted.org/packages/9f/56/1dc2fa53685dca9c3f243a6cbecd34e856858354e455b77f47ebd76cf5bf/msgspec-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb33b5eb5adb3c33d749684471c6a165468395d7aa02d8867c15103b81e1da3e", size = 227248, upload-time = "2025-11-24T03:55:39.496Z" }, ] [[package]] @@ -1302,11 +1259,11 @@ wheels = [ [[package]] name = "networkx" -version = "3.5" +version = "3.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] [[package]] @@ -1315,7 +1272,6 @@ version = "1.13.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/73/79a0b22fc731989c708068427579e840a6cf4e937fe7ae5c5d0b7356ac22/ninja-1.13.0.tar.gz", hash = "sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978", size = 242558, upload-time = "2025-08-11T15:10:19.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/74/d02409ed2aa865e051b7edda22ad416a39d81a84980f544f8de717cab133/ninja-1.13.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1", size = 310125, upload-time = "2025-08-11T15:09:50.971Z" }, { url = "https://files.pythonhosted.org/packages/8e/de/6e1cd6b84b412ac1ef327b76f0641aeb5dcc01e9d3f9eee0286d0c34fd93/ninja-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630", size = 177467, upload-time = "2025-08-11T15:09:52.767Z" }, { url = "https://files.pythonhosted.org/packages/c8/83/49320fb6e58ae3c079381e333575fdbcf1cca3506ee160a2dcce775046fa/ninja-1.13.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c", size = 187834, upload-time = "2025-08-11T15:09:54.115Z" }, { url = "https://files.pythonhosted.org/packages/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" }, @@ -1330,9 +1286,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/fb/95752eb635bb8ad27d101d71bef15bc63049de23f299e312878fc21cb2da/ninja-1.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d741a5e6754e0bda767e3274a0f0deeef4807f1fec6c0d7921a0244018926ae5", size = 585106, upload-time = "2025-08-11T15:10:09.818Z" }, { url = "https://files.pythonhosted.org/packages/c1/31/aa56a1a286703800c0cbe39fb4e82811c277772dc8cd084f442dd8e2938a/ninja-1.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:e8bad11f8a00b64137e9b315b137d8bb6cbf3086fbdc43bf1f90fd33324d2e96", size = 707138, upload-time = "2025-08-11T15:10:11.366Z" }, { url = "https://files.pythonhosted.org/packages/34/6f/5f5a54a1041af945130abdb2b8529cbef0cdcbbf9bcf3f4195378319d29a/ninja-1.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b4f2a072db3c0f944c32793e91532d8948d20d9ab83da9c0c7c15b5768072200", size = 581758, upload-time = "2025-08-11T15:10:13.295Z" }, - { url = "https://files.pythonhosted.org/packages/95/97/51359c77527d45943fe7a94d00a3843b81162e6c4244b3579fe8fc54cb9c/ninja-1.13.0-py3-none-win32.whl", hash = "sha256:8cfbb80b4a53456ae8a39f90ae3d7a2129f45ea164f43fadfa15dc38c4aef1c9", size = 267201, upload-time = "2025-08-11T15:10:15.158Z" }, - { url = "https://files.pythonhosted.org/packages/29/45/c0adfbfb0b5895aa18cec400c535b4f7ff3e52536e0403602fc1a23f7de9/ninja-1.13.0-py3-none-win_amd64.whl", hash = "sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e", size = 309975, upload-time = "2025-08-11T15:10:16.697Z" }, - { url = "https://files.pythonhosted.org/packages/df/93/a7b983643d1253bb223234b5b226e69de6cda02b76cdca7770f684b795f5/ninja-1.13.0-py3-none-win_arm64.whl", hash = "sha256:3c0b40b1f0bba764644385319028650087b4c1b18cdfa6f45cb39a3669b81aa9", size = 290806, upload-time = "2025-08-11T15:10:18.018Z" }, ] [[package]] @@ -1340,34 +1293,47 @@ name = "numba" version = "0.61.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "llvmlite" }, - { name = "numpy" }, + { name = "llvmlite", marker = "sys_platform == 'linux'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, ] 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" }, ] [[package]] name = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform == 'linux'", +] 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" }, +] + +[[package]] +name = "numpy" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'darwin' and sys_platform != 'linux'", + "sys_platform == 'darwin'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/7a/6a3d14e205d292b738db449d0de649b373a59edb0d0b4493821d0a3e8718/numpy-2.4.0.tar.gz", hash = "sha256:6e504f7b16118198f138ef31ba24d985b124c2c469fe8467007cf30fd992f934", size = 20685720, upload-time = "2025-12-20T16:18:19.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ff/f6400ffec95de41c74b8e73df32e3fff1830633193a7b1e409be7fb1bb8c/numpy-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a8b6bb8369abefb8bd1801b054ad50e02b3275c8614dc6e5b0373c305291037", size = 16653117, upload-time = "2025-12-20T16:16:06.709Z" }, + { url = "https://files.pythonhosted.org/packages/fd/28/6c23e97450035072e8d830a3c411bf1abd1f42c611ff9d29e3d8f55c6252/numpy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e284ca13d5a8367e43734148622caf0b261b275673823593e3e3634a6490f83", size = 12369711, upload-time = "2025-12-20T16:16:08.758Z" }, + { url = "https://files.pythonhosted.org/packages/bc/af/acbef97b630ab1bb45e6a7d01d1452e4251aa88ce680ac36e56c272120ec/numpy-2.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:49ff32b09f5aa0cd30a20c2b39db3e669c845589f2b7fc910365210887e39344", size = 5198355, upload-time = "2025-12-20T16:16:10.902Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c8/4e0d436b66b826f2e53330adaa6311f5cac9871a5b5c31ad773b27f25a74/numpy-2.4.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:36cbfb13c152b1c7c184ddac43765db8ad672567e7bafff2cc755a09917ed2e6", size = 6545298, upload-time = "2025-12-20T16:16:12.607Z" }, + { url = "https://files.pythonhosted.org/packages/d3/6f/f868765d44e6fc466467ed810ba9d8d6db1add7d4a748abfa2a4c99a3194/numpy-2.4.0-cp312-cp312-win32.whl", hash = "sha256:92190db305a6f48734d3982f2c60fa30d6b5ee9bff10f2887b930d7b40119f4c", size = 5955432, upload-time = "2025-12-20T16:16:25.06Z" }, + { url = "https://files.pythonhosted.org/packages/d4/b5/94c1e79fcbab38d1ca15e13777477b2914dd2d559b410f96949d6637b085/numpy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:680060061adb2d74ce352628cb798cfdec399068aa7f07ba9fb818b2b3305f98", size = 12306201, upload-time = "2025-12-20T16:16:26.979Z" }, + { url = "https://files.pythonhosted.org/packages/70/09/c39dadf0b13bb0768cd29d6a3aaff1fb7c6905ac40e9aaeca26b1c086e06/numpy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:39699233bc72dd482da1415dcb06076e32f60eddc796a796c5fb6c5efce94667", size = 10308234, upload-time = "2025-12-20T16:16:29.417Z" }, ] [[package]] @@ -1375,6 +1341,7 @@ name = "nvidia-cublas-cu12" version = "12.8.4.1" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/29/99/db44d685f0e257ff0e213ade1964fc459b4a690a73293220e98feb3307cf/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0", size = 590537124, upload-time = "2025-03-07T01:43:53.556Z" }, { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, ] @@ -1383,6 +1350,7 @@ name = "nvidia-cuda-cupti-cu12" version = "12.8.90" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1f/b3bd73445e5cb342727fd24fe1f7b748f690b460acadc27ea22f904502c8/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed", size = 9533318, upload-time = "2025-03-07T01:40:10.421Z" }, { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, ] @@ -1392,6 +1360,7 @@ version = "12.8.93" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d1/e50d0acaab360482034b84b6e27ee83c6738f7d32182b987f9c7a4e32962/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8", size = 43106076, upload-time = "2025-03-07T01:41:59.817Z" }, ] [[package]] @@ -1399,6 +1368,7 @@ name = "nvidia-cuda-runtime-cu12" version = "12.8.90" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/75/f865a3b236e4647605ea34cc450900854ba123834a5f1598e160b9530c3a/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d", size = 965265, upload-time = "2025-03-07T01:39:43.533Z" }, { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, ] @@ -1407,20 +1377,20 @@ name = "nvidia-cudnn-cu12" version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/41/e79269ce215c857c935fd86bcfe91a451a584dfc27f1e068f568b9ad1ab7/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8", size = 705026878, upload-time = "2025-06-06T21:52:51.348Z" }, { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, ] [[package]] name = "nvidia-cudnn-frontend" -version = "1.16.0" +version = "1.17.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/42/d9/f58ed6292c9396f7422812a0a2d9f80cc5a623ea6c758bcb3d34d4795bb8/nvidia_cudnn_frontend-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de0c473f32d705abcf14f351615f7ffbeed7320e3499cf2195ae5689652a2592", size = 1917620, upload-time = "2025-12-20T00:27:46.179Z" }, + { url = "https://files.pythonhosted.org/packages/db/eb/c641135632bd2afc21339aadee96af4c5db1460dfa07ca74836de75a590f/nvidia_cudnn_frontend-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c913c87fca691a91385287f2587575531933acfebc85c33dbcecb191886c7a53", size = 2038994, upload-time = "2025-12-20T00:25:18.9Z" }, ] [[package]] @@ -1428,9 +1398,10 @@ name = "nvidia-cufft-cu12" version = "11.3.3.83" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/60/bc/7771846d3a0272026c416fbb7e5f4c1f146d6d80704534d0b187dd6f4800/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a", size = 193109211, upload-time = "2025-03-07T01:44:56.873Z" }, { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, ] @@ -1440,6 +1411,7 @@ version = "1.13.1.3" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f5/5607710447a6fe9fd9b3283956fceeee8a06cda1d2f56ce31371f595db2a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a", size = 1120705, upload-time = "2025-03-07T01:45:41.434Z" }, ] [[package]] @@ -1447,6 +1419,7 @@ name = "nvidia-curand-cu12" version = "10.3.9.90" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/45/5e/92aa15eca622a388b80fbf8375d4760738df6285b1e92c43d37390a33a9a/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd", size = 63625754, upload-time = "2025-03-07T01:46:10.735Z" }, { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, ] @@ -1455,11 +1428,12 @@ name = "nvidia-cusolver-cu12" version = "11.7.3.90" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, - { name = "nvidia-cusparse-cu12" }, - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/32/f7cd6ce8a7690544d084ea21c26e910a97e077c9b7f07bf5de623ee19981/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0", size = 267229841, upload-time = "2025-03-07T01:46:54.356Z" }, { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, ] @@ -1468,9 +1442,10 @@ name = "nvidia-cusparse-cu12" version = "12.5.8.93" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/f7/cd777c4109681367721b00a106f491e0d0d15cfa1fd59672ce580ce42a97/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc", size = 288117129, upload-time = "2025-03-07T01:47:40.407Z" }, { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, ] @@ -1479,21 +1454,22 @@ name = "nvidia-cusparselt-cu12" version = "0.7.1" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/73/b9/598f6ff36faaece4b3c50d26f50e38661499ff34346f00e057760b35cc9d/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5", size = 283835557, upload-time = "2025-02-26T00:16:54.265Z" }, { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, ] [[package]] name = "nvidia-cutlass-dsl" -version = "4.3.3" +version = "4.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-python" }, - { name = "numpy" }, - { name = "typing-extensions" }, + { name = "cuda-python", marker = "sys_platform == 'linux'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/d4/7c5ef53ccf75d7f99a9ea29cae9f9c0233229b75b3b22f85a4ef4f52e6ab/nvidia_cutlass_dsl-4.3.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3278526f54bddd920d8e539771e5820c6166c549a1e67813375025f39417dec6", size = 58734009, upload-time = "2025-12-10T09:23:29.305Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a27562194cc4182c67793cd21c5dbf9468cd5a49c775a487153c6f28364c/nvidia_cutlass_dsl-4.3.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f2b25816b8bb8bc332bcbf6fc341347b5d728344cf185c65af0dd73e8503d5c7", size = 58596724, upload-time = "2025-12-10T11:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/86/ee/53d22e2e14cb763927d85f7ec9748f6af6d27a2b7f43d52de014728da10e/nvidia_cutlass_dsl-4.3.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:57693d87677919572ab9eefa386b3f39e8e888bc4a9db7ab8730a97e8dbe06b4", size = 58736300, upload-time = "2025-12-21T07:41:25.723Z" }, + { url = "https://files.pythonhosted.org/packages/66/f6/47489e07081cd4060f08bfa4166f8ff32beaecf71c06060d03bde88f3b6c/nvidia_cutlass_dsl-4.3.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a48fbff859e44dd548f8f26819d97d0595acea70e3b057c91dfdb47929015c72", size = 58599014, upload-time = "2025-12-21T07:38:51.632Z" }, ] [[package]] @@ -1510,6 +1486,7 @@ name = "nvidia-nccl-cu12" version = "2.27.5" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/1c/857979db0ef194ca5e21478a0612bcdbbe59458d7694361882279947b349/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:31432ad4d1fb1004eb0c56203dc9bc2178a1ba69d1d9e02d64a6938ab5e40e7a", size = 322400625, upload-time = "2025-06-26T04:11:04.496Z" }, { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, ] @@ -1519,6 +1496,7 @@ version = "12.8.93" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a2/8cee5da30d13430e87bf99bb33455d2724d0a4a9cb5d7926d80ccb96d008/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7", size = 38386204, upload-time = "2025-03-07T01:49:43.612Z" }, ] [[package]] @@ -1526,6 +1504,7 @@ name = "nvidia-nvshmem-cu12" version = "3.3.20" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/92/9d/3dd98852568fb845ec1f7902c90a22b240fe1cbabda411ccedf2fd737b7b/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b0b960da3842212758e4fa4696b94f129090b30e5122fea3c5345916545cff0", size = 124484616, upload-time = "2025-08-04T20:24:59.172Z" }, { url = "https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5", size = 124657145, upload-time = "2025-08-04T20:25:19.995Z" }, ] @@ -1534,12 +1513,13 @@ name = "nvidia-nvtx-cu12" version = "12.8.90" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/10/c0/1b303feea90d296f6176f32a2a70b5ef230f9bdeb3a72bddb0dc922dc137/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615", size = 91161, upload-time = "2025-03-07T01:42:23.922Z" }, { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, ] [[package]] name = "openai" -version = "2.7.1" +version = "2.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1551,9 +1531,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/a2/f4023c1e0c868a6a5854955b3374f17153388aed95e835af114a17eac95b/openai-2.7.1.tar.gz", hash = "sha256:df4d4a3622b2df3475ead8eb0fbb3c27fd1c070fa2e55d778ca4f40e0186c726", size = 595933, upload-time = "2025-11-04T06:07:23.069Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/b1/12fe1c196bea326261718eb037307c1c1fe1dedc2d2d4de777df822e6238/openai-2.14.0.tar.gz", hash = "sha256:419357bedde9402d23bf8f2ee372fca1985a73348debba94bddff06f19459952", size = 626938, upload-time = "2025-12-19T03:28:45.742Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/74/6bfc3adc81f6c2cea4439f2a734c40e3a420703bbcdc539890096a732bbd/openai-2.7.1-py3-none-any.whl", hash = "sha256:2f2530354d94c59c614645a4662b9dab0a5b881c5cd767a8587398feac0c9021", size = 1008780, upload-time = "2025-11-04T06:07:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/27/4b/7c1a00c2c3fbd004253937f7520f692a9650767aa73894d7a34f0d65d3f4/openai-2.14.0-py3-none-any.whl", hash = "sha256:7ea40aca4ffc4c4a776e77679021b47eec1160e341f42ae086ba949c9dcc9183", size = 1067558, upload-time = "2025-12-19T03:28:43.727Z" }, ] [[package]] @@ -1561,11 +1541,10 @@ name = "openai-harmony" version = "0.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, + { name = "pydantic", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3e/92/2d038d096f29179c7c9571b431f9e739f87a487121901725e23fe338dd9d/openai_harmony-0.0.8.tar.gz", hash = "sha256:6e43f98e6c242fa2de6f8ea12eab24af63fa2ed3e89c06341fb9d92632c5cbdf", size = 284777, upload-time = "2025-11-05T19:07:06.727Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/c6/2502f416d46be3ec08bb66d696cccffb57781a499e3ff2e4d7c174af4e8f/openai_harmony-0.0.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:029ec25ca74abe48fdb58eb9fdd2a8c1618581fc33ce8e5653f8a1ffbfbd9326", size = 2627806, upload-time = "2025-11-05T19:06:57.063Z" }, { url = "https://files.pythonhosted.org/packages/d3/d2/ce6953ca87db9cae3e775024184da7d1c5cb88cead19a2d75b42f00a959c/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4f709815924ec325b9a890e6ab2bbb0ceec8e319a4e257328eb752cf36b2efc", size = 2948463, upload-time = "2025-11-05T19:06:48.17Z" }, { url = "https://files.pythonhosted.org/packages/fa/4c/b553c9651662d6ce102ca7f3629d268b23df1abe5841e24bed81e8a8e949/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5cfcfd963b50a41fc656c84d3440ca6eecdccd6c552158ce790b8f2e33dfb5a9", size = 2704083, upload-time = "2025-11-05T19:06:50.205Z" }, { url = "https://files.pythonhosted.org/packages/9b/af/4eec8f9ab9c27bcdb444460c72cf43011d176fc44c79d6e113094ca1e152/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a3a16972aa1cee38ea958470cd04ac9a2d5ac38fdcf77ab686611246220c158", size = 2959765, upload-time = "2025-11-05T19:06:53.62Z" }, @@ -1575,8 +1554,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1d/10/4327dbf87f75ae813405fd9a9b4a5cde63d506ffed0a096a440a4cabd89c/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:cbaa3bda75ef0d8836e1f8cc84af62f971b1d756d740efc95c38c3e04c0bfde2", size = 2932931, upload-time = "2025-11-05T19:07:01.437Z" }, { url = "https://files.pythonhosted.org/packages/8a/c8/1774eec4f6f360ef57618fb8f52e3d3af245b2491bd0297513aa09eec04b/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:772922a9bd24e133950fad71eb1550836f415a88e8c77870e12d0c3bd688ddc2", size = 2996140, upload-time = "2025-11-05T19:07:03.438Z" }, { url = "https://files.pythonhosted.org/packages/60/c3/3d1e01e2dba517a91760e4a03e4f20ffc75039a6fe584d0e6f9b5c78fd15/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:007b0476a1f331f8130783f901f1da6f5a7057af1a4891f1b6a31dec364189b5", size = 3205080, upload-time = "2025-11-05T19:07:05.078Z" }, - { url = "https://files.pythonhosted.org/packages/14/63/119de431572d7c70a7bf1037034a9be6ed0a7502a7498ba7302bca5b3242/openai_harmony-0.0.8-cp38-abi3-win32.whl", hash = "sha256:a9b5f893326b28d9e935ade14b4f655f5a840942473bc89b201c25f7a15af9cf", size = 2082457, upload-time = "2025-11-05T19:07:09.631Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/c83cf5a206c263ee70448a5ae4264682555f4d0b5bed0d2cc6ca1108103d/openai_harmony-0.0.8-cp38-abi3-win_amd64.whl", hash = "sha256:39d44f0d8f466bd56698e7ead708bead3141e27b9b87e3ab7d5a6d0e4a869ee5", size = 2438369, upload-time = "2025-11-05T19:07:08.1Z" }, ] [[package]] @@ -1584,16 +1561,12 @@ name = "opencv-python-headless" version = "4.12.0.88" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a4/63/6861102ec149c3cd298f4d1ea7ce9d6adbc7529221606ff1dab991a19adb/opencv-python-headless-4.12.0.88.tar.gz", hash = "sha256:cfdc017ddf2e59b6c2f53bc12d74b6b0be7ded4ec59083ea70763921af2b6c09", size = 95379675, upload-time = "2025-07-07T09:21:06.815Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/7d/414e243c5c8216a5277afd104a319cc1291c5e23f5eeef512db5629ee7f4/opencv_python_headless-4.12.0.88-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1e58d664809b3350c1123484dd441e1667cd7bed3086db1b9ea1b6f6cb20b50e", size = 37877864, upload-time = "2025-07-07T09:14:41.693Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/7e162714beed1cd5e7b5eb66fcbcba2f065c51b1d9da2463024c84d2f7c0/opencv_python_headless-4.12.0.88-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:365bb2e486b50feffc2d07a405b953a8f3e8eaa63865bc650034e5c71e7a5154", size = 57326608, upload-time = "2025-07-07T09:14:51.885Z" }, { url = "https://files.pythonhosted.org/packages/69/4e/116720df7f1f7f3b59abc608ca30fbec9d2b3ae810afe4e4d26483d9dfa0/opencv_python_headless-4.12.0.88-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:aeb4b13ecb8b4a0beb2668ea07928160ea7c2cd2d9b5ef571bbee6bafe9cc8d0", size = 33145800, upload-time = "2025-07-07T09:15:00.367Z" }, { url = "https://files.pythonhosted.org/packages/89/53/e19c21e0c4eb1275c3e2c97b081103b6dfb3938172264d283a519bf728b9/opencv_python_headless-4.12.0.88-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:236c8df54a90f4d02076e6f9c1cc763d794542e886c576a6fee46ec8ff75a7a9", size = 54023419, upload-time = "2025-07-07T09:15:10.164Z" }, - { url = "https://files.pythonhosted.org/packages/bf/9c/a76fd5414de6ec9f21f763a600058a0c3e290053cea87e0275692b1375c0/opencv_python_headless-4.12.0.88-cp37-abi3-win32.whl", hash = "sha256:fde2cf5c51e4def5f2132d78e0c08f9c14783cd67356922182c6845b9af87dbd", size = 30225230, upload-time = "2025-07-07T09:15:17.045Z" }, - { url = "https://files.pythonhosted.org/packages/f2/35/0858e9e71b36948eafbc5e835874b63e515179dc3b742cbe3d76bc683439/opencv_python_headless-4.12.0.88-cp37-abi3-win_amd64.whl", hash = "sha256:86b413bdd6c6bf497832e346cd5371995de148e579b9774f8eba686dee3f5528", size = 38923559, upload-time = "2025-07-07T09:15:25.229Z" }, ] [[package]] @@ -1602,14 +1575,8 @@ version = "0.2.11" source = { registry = "https://pypi.org/simple" } 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" }, ] [[package]] @@ -1626,7 +1593,8 @@ name = "pandas" version = "2.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, + { name = "numpy", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, { name = "python-dateutil" }, { name = "pytz" }, { name = "tzdata" }, @@ -1644,11 +1612,11 @@ wheels = [ [[package]] name = "partial-json-parser" -version = "0.2.1.1.post6" +version = "0.2.1.1.post7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/13/459e86c9c67a006651803a3df3d0b08f7708bc5483fdc482582d75562949/partial_json_parser-0.2.1.1.post6.tar.gz", hash = "sha256:43896b68929678224cbbe4884a6a5fe9251ded4b30b8b7d7eb569e5feea93afc", size = 10299, upload-time = "2025-06-23T17:51:45.372Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/6d/eed37d7ebc1e0bcd27b831c0cf1fe94881934316187c4b30d23f29ea0bd4/partial_json_parser-0.2.1.1.post7.tar.gz", hash = "sha256:86590e1ba6bcb6739a2dfc17d2323f028cb5884f4c6ce23db376999132c9a922", size = 10296, upload-time = "2025-11-17T07:27:41.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/40/1f922794af3dc7503f19319a8804b398a161a2cd54183cff8b12225b8d85/partial_json_parser-0.2.1.1.post6-py3-none-any.whl", hash = "sha256:abc332f09b13ef5233384dbfe7128a0e9ea3fa4b8f8be9b37ac1b433c810e99e", size = 10876, upload-time = "2025-06-23T17:51:44.332Z" }, + { url = "https://files.pythonhosted.org/packages/42/32/658973117bf0fd82a24abbfb94fe73a5e86216e49342985e10acce54775a/partial_json_parser-0.2.1.1.post7-py3-none-any.whl", hash = "sha256:145119e5eabcf80cbb13844a6b50a85c68bf99d376f8ed771e2a3c3b03e653ae", size = 10877, upload-time = "2025-11-17T07:27:40.457Z" }, ] [[package]] @@ -1658,12 +1626,15 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate" }, { name = "huggingface-hub" }, - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, + { name = "numpy", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, { name = "packaging" }, { name = "psutil" }, { name = "pyyaml" }, { name = "safetensors" }, - { name = "torch" }, + { name = "torch", version = "2.9.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.9.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { name = "torch", version = "2.9.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin' and sys_platform != 'linux'" }, { name = "tqdm" }, { name = "transformers" }, ] @@ -1678,17 +1649,21 @@ version = "12.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, - { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] [[package]] @@ -1714,8 +1689,8 @@ name = "prometheus-fastapi-instrumentator" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "prometheus-client" }, - { name = "starlette" }, + { name = "prometheus-client", marker = "sys_platform == 'linux'" }, + { name = "starlette", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/69/6d/24d53033cf93826aa7857699a4450c1c67e5b9c710e925b1ed2b320c04df/prometheus_fastapi_instrumentator-7.1.0.tar.gz", hash = "sha256:be7cd61eeea4e5912aeccb4261c6631b3f227d8924542d79eaf5af3f439cbe5e", size = 20220, upload-time = "2025-03-19T19:35:05.351Z" } wheels = [ @@ -1748,31 +1723,33 @@ wheels = [ [[package]] name = "protobuf" -version = "6.33.0" +version = "6.33.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/ff/64a6c8f420818bb873713988ca5492cba3a7946be57e027ac63495157d97/protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954", size = 443463, upload-time = "2025-10-15T20:39:52.159Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/44/e49ecff446afeec9d1a66d6bbf9adc21e3c7cea7803a920ca3773379d4f6/protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4", size = 444296, upload-time = "2025-12-06T00:17:53.311Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/ee/52b3fa8feb6db4a833dfea4943e175ce645144532e8a90f72571ad85df4e/protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035", size = 425593, upload-time = "2025-10-15T20:39:40.29Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c6/7a465f1825872c55e0341ff4a80198743f73b69ce5d43ab18043699d1d81/protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee", size = 436882, upload-time = "2025-10-15T20:39:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/e1/a9/b6eee662a6951b9c3640e8e452ab3e09f117d99fc10baa32d1581a0d4099/protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455", size = 427521, upload-time = "2025-10-15T20:39:43.803Z" }, - { url = "https://files.pythonhosted.org/packages/10/35/16d31e0f92c6d2f0e77c2a3ba93185130ea13053dd16200a57434c882f2b/protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90", size = 324445, upload-time = "2025-10-15T20:39:44.932Z" }, - { url = "https://files.pythonhosted.org/packages/e6/eb/2a981a13e35cda8b75b5585aaffae2eb904f8f351bdd3870769692acbd8a/protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298", size = 339159, upload-time = "2025-10-15T20:39:46.186Z" }, - { url = "https://files.pythonhosted.org/packages/21/51/0b1cbad62074439b867b4e04cc09b93f6699d78fd191bed2bbb44562e077/protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef", size = 323172, upload-time = "2025-10-15T20:39:47.465Z" }, - { url = "https://files.pythonhosted.org/packages/07/d1/0a28c21707807c6aacd5dc9c3704b2aa1effbf37adebd8caeaf68b17a636/protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995", size = 170477, upload-time = "2025-10-15T20:39:51.311Z" }, + { url = "https://files.pythonhosted.org/packages/bc/91/1e3a34881a88697a7354ffd177e8746e97a722e5e8db101544b47e84afb1/protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d", size = 425603, upload-time = "2025-12-06T00:17:41.114Z" }, + { url = "https://files.pythonhosted.org/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4", size = 436930, upload-time = "2025-12-06T00:17:43.278Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43", size = 427621, upload-time = "2025-12-06T00:17:44.445Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e", size = 324460, upload-time = "2025-12-06T00:17:45.678Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fa/26468d00a92824020f6f2090d827078c09c9c587e34cbfd2d0c7911221f8/protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872", size = 339168, upload-time = "2025-12-06T00:17:46.813Z" }, + { url = "https://files.pythonhosted.org/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f", size = 323270, upload-time = "2025-12-06T00:17:48.253Z" }, + { url = "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", size = 170501, upload-time = "2025-12-06T00:17:52.211Z" }, ] [[package]] name = "psutil" -version = "7.1.3" +version = "7.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059, upload-time = "2025-11-02T12:25:54.619Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/7c/31d1c3ceb1260301f87565f50689dc6da3db427ece1e1e012af22abca54e/psutil-7.2.0.tar.gz", hash = "sha256:2e4f8e1552f77d14dc96fb0f6240c5b34a37081c0889f0853b3b29a496e5ef64", size = 489863, upload-time = "2025-12-23T20:26:24.616Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359, upload-time = "2025-11-02T12:26:25.284Z" }, - { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171, upload-time = "2025-11-02T12:26:27.23Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261, upload-time = "2025-11-02T12:26:29.48Z" }, - { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635, upload-time = "2025-11-02T12:26:31.74Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633, upload-time = "2025-11-02T12:26:33.887Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" }, + { url = "https://files.pythonhosted.org/packages/40/c5/a49160bf3e165b7b93a60579a353cf5d939d7f878fe5fd369110f1d18043/psutil-7.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:977a2fcd132d15cb05b32b2d85b98d087cad039b0ce435731670ba74da9e6133", size = 128116, upload-time = "2025-12-23T20:26:53.516Z" }, + { url = "https://files.pythonhosted.org/packages/10/a1/c75feb480f60cd768fb6ed00ac362a16a33e5076ec8475a22d8162fb2659/psutil-7.2.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:24151011c21fadd94214d7139d7c6c54569290d7e553989bdf0eab73b13beb8c", size = 128925, upload-time = "2025-12-23T20:26:55.573Z" }, + { url = "https://files.pythonhosted.org/packages/12/ff/e93136587c00a543f4bc768b157fac2c47cd77b180d4f4e5c6efb6ea53a2/psutil-7.2.0-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91f211ba9279e7c61d9d8f84b713cfc38fa161cb0597d5cb3f1ca742f6848254", size = 154666, upload-time = "2025-12-23T20:26:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/b8/dd/4c2de9c3827c892599d277a69d2224136800870a8a88a80981de905de28d/psutil-7.2.0-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f37415188b7ea98faf90fed51131181646c59098b077550246e2e092e127418b", size = 156109, upload-time = "2025-12-23T20:26:58.851Z" }, + { url = "https://files.pythonhosted.org/packages/81/3f/090943c682d3629968dd0b04826ddcbc760ee1379021dbe316e2ddfcd01b/psutil-7.2.0-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0d12c7ce6ed1128cd81fd54606afa054ac7dbb9773469ebb58cf2f171c49f2ac", size = 148081, upload-time = "2025-12-23T20:27:01.318Z" }, + { url = "https://files.pythonhosted.org/packages/c4/88/c39648ebb8ec182d0364af53cdefe6eddb5f3872ba718b5855a8ff65d6d4/psutil-7.2.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ca0faef7976530940dcd39bc5382d0d0d5eb023b186a4901ca341bd8d8684151", size = 147376, upload-time = "2025-12-23T20:27:03.347Z" }, + { url = "https://files.pythonhosted.org/packages/01/a2/5b39e08bd9b27476bc7cce7e21c71a481ad60b81ffac49baf02687a50d7f/psutil-7.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:abdb74137ca232d20250e9ad471f58d500e7743bc8253ba0bfbf26e570c0e437", size = 136910, upload-time = "2025-12-23T20:27:05.289Z" }, + { url = "https://files.pythonhosted.org/packages/59/54/53839db1258c1eaeb4ded57ff202144ebc75b23facc05a74fd98d338b0c6/psutil-7.2.0-cp37-abi3-win_arm64.whl", hash = "sha256:284e71038b3139e7ab3834b63b3eb5aa5565fcd61a681ec746ef9a0a8c457fd2", size = 133807, upload-time = "2025-12-23T20:27:06.825Z" }, ] [[package]] @@ -1801,29 +1778,26 @@ wheels = [ [[package]] name = "pybase64" -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" }, +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/b8/4ed5c7ad5ec15b08d35cc79ace6145d5c1ae426e46435f4987379439dfea/pybase64-1.4.3.tar.gz", hash = "sha256:c2ed274c9e0ba9c8f9c4083cfe265e66dd679126cd9c2027965d807352f3f053", size = 137272, upload-time = "2025-12-06T13:27:04.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/1b/9a8cab0042b464e9a876d5c65fe5127445a2436da36fda64899b119b1a1b/pybase64-1.4.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f0b3f200c3e06316f6bebabd458b4e4bcd4c2ca26af7c0c766614d91968dee27", size = 68210, upload-time = "2025-12-06T13:23:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/62/f7/965b79ff391ad208b50e412b5d3205ccce372a2d27b7218ae86d5295b105/pybase64-1.4.3-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb632edfd132b3eaf90c39c89aa314beec4e946e210099b57d40311f704e11d4", size = 71599, upload-time = "2025-12-06T13:23:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/03/4b/a3b5175130b3810bbb8ccfa1edaadbd3afddb9992d877c8a1e2f274b476e/pybase64-1.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:356ef1d74648ce997f5a777cf8f1aefecc1c0b4fe6201e0ef3ec8a08170e1b54", size = 59922, upload-time = "2025-12-06T13:23:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/5d/c38d1572027fc601b62d7a407721688b04b4d065d60ca489912d6893e6cf/pybase64-1.4.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:c48361f90db32bacaa5518419d4eb9066ba558013aaf0c7781620279ecddaeb9", size = 56712, upload-time = "2025-12-06T13:23:22.77Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d4/4e04472fef485caa8f561d904d4d69210a8f8fc1608ea15ebd9012b92655/pybase64-1.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:702bcaa16ae02139d881aeaef5b1c8ffb4a3fae062fe601d1e3835e10310a517", size = 59300, upload-time = "2025-12-06T13:23:24.543Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/16e29721b86734b881d09b7e23dfd7c8408ad01a4f4c7525f3b1088e25ec/pybase64-1.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:53d0ffe1847b16b647c6413d34d1de08942b7724273dd57e67dcbdb10c574045", size = 60278, upload-time = "2025-12-06T13:23:25.608Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/18515f211d7c046be32070709a8efeeef8a0203de4fd7521e6b56404731b/pybase64-1.4.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9a1792e8b830a92736dae58f0c386062eb038dfe8004fb03ba33b6083d89cd43", size = 54817, upload-time = "2025-12-06T13:23:26.633Z" }, + { url = "https://files.pythonhosted.org/packages/e7/be/14e29d8e1a481dbff151324c96dd7b5d2688194bb65dc8a00ca0e1ad1e86/pybase64-1.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d468b1b1ac5ad84875a46eaa458663c3721e8be5f155ade356406848d3701f6", size = 58611, upload-time = "2025-12-06T13:23:27.684Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8a/a2588dfe24e1bbd742a554553778ab0d65fdf3d1c9a06d10b77047d142aa/pybase64-1.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e97b7bdbd62e71898cd542a6a9e320d9da754ff3ebd02cb802d69087ee94d468", size = 52404, upload-time = "2025-12-06T13:23:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/afcda7445bebe0cbc38cafdd7813234cdd4fc5573ff067f1abf317bb0cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b33aeaa780caaa08ffda87fc584d5eab61e3d3bbb5d86ead02161dc0c20d04bc", size = 68817, upload-time = "2025-12-06T13:23:30.079Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3a/87c3201e555ed71f73e961a787241a2438c2bbb2ca8809c29ddf938a3157/pybase64-1.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c0efcf78f11cf866bed49caa7b97552bc4855a892f9cc2372abcd3ed0056f0d", size = 57854, upload-time = "2025-12-06T13:23:31.17Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7d/931c2539b31a7b375e7d595b88401eeb5bd6c5ce1059c9123f9b608aaa14/pybase64-1.4.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:66e3791f2ed725a46593f8bd2761ff37d01e2cdad065b1dceb89066f476e50c6", size = 54333, upload-time = "2025-12-06T13:23:32.422Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/537601e02cc01f27e9d75f440f1a6095b8df44fc28b1eef2cd739aea8cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:72bb0b6bddadab26e1b069bb78e83092711a111a80a0d6b9edcb08199ad7299b", size = 56492, upload-time = "2025-12-06T13:23:33.515Z" }, + { url = "https://files.pythonhosted.org/packages/96/97/2a2e57acf8f5c9258d22aba52e71f8050e167b29ed2ee1113677c1b600c1/pybase64-1.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5b3365dbcbcdb0a294f0f50af0c0a16b27a232eddeeb0bceeefd844ef30d2a23", size = 70974, upload-time = "2025-12-06T13:23:36.27Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8f/43c3bb11ca9bacf81cb0b7a71500bb65b2eda6d5fe07433c09b543de97f3/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5c29a582b0ea3936d02bd6fe9bf674ab6059e6e45ab71c78404ab2c913224414", size = 43461, upload-time = "2025-12-06T13:26:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4c/2a5258329200be57497d3972b5308558c6de42e3749c6cc2aa1cbe34b25a/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6b664758c804fa919b4f1257aa8cf68e95db76fc331de5f70bfc3a34655afe1", size = 36058, upload-time = "2025-12-06T13:26:30.092Z" }, ] [[package]] @@ -1846,7 +1820,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.4" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1854,14 +1828,14 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [package.optional-dependencies] email = [ - { name = "email-validator" }, + { name = "email-validator", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -1898,8 +1872,8 @@ name = "pydantic-extra-types" version = "2.10.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, - { name = "typing-extensions" }, + { name = "pydantic", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3a/10/fb64987804cde41bcc39d9cd757cd5f2bb5d97b389d81aa70238b14b8a7e/pydantic_extra_types-2.10.6.tar.gz", hash = "sha256:c63d70bf684366e6bbe1f4ee3957952ebe6973d41e7802aea0b770d06b116aeb", size = 141858, upload-time = "2025-10-08T13:47:49.483Z" } wheels = [ @@ -1908,7 +1882,7 @@ wheels = [ [package.optional-dependencies] pycountry = [ - { name = "pycountry" }, + { name = "pycountry", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -1916,9 +1890,9 @@ name = "pydantic-settings" version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, + { name = "pydantic", marker = "sys_platform == 'linux'" }, + { name = "python-dotenv", marker = "sys_platform == 'linux'" }, + { name = "typing-inspection", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } wheels = [ @@ -1945,12 +1919,12 @@ wheels = [ [package.optional-dependencies] crypto = [ - { name = "cryptography" }, + { name = "cryptography", marker = "sys_platform == 'linux'" }, ] [[package]] name = "pytest" -version = "8.4.2" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1959,22 +1933,22 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] name = "pytest-asyncio" -version = "1.2.0" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, + { 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" }, ] [[package]] @@ -2009,11 +1983,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.20" +version = "0.0.21" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, ] [[package]] @@ -2058,55 +2032,40 @@ name = "pyzmq" version = "27.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "implementation_name == 'pypy'" }, + { name = "cffi", marker = "implementation_name == 'pypy' and sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, - { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, - { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, ] [[package]] name = "ray" -version = "2.51.1" +version = "2.53.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "filelock" }, - { name = "jsonschema" }, - { name = "msgpack" }, - { name = "packaging" }, - { name = "protobuf" }, - { name = "pyyaml" }, - { name = "requests" }, + { name = "click", marker = "sys_platform == 'linux'" }, + { name = "filelock", marker = "sys_platform == 'linux'" }, + { name = "jsonschema", marker = "sys_platform == 'linux'" }, + { name = "msgpack", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "protobuf", marker = "sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/95/51e44ce79e42f02ca1c4d4c5501e6dd49f3a384c5f6324aceb4e0015988a/ray-2.51.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ef847b025ca758baee4571a1ca001d973897cad772f8e95d7f303d24c38b649e", size = 68029226, upload-time = "2025-11-01T03:24:21.928Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b5/a93e39e131067edb7cba3385a609f61aaaf7aa54728cd3a7474bfbf3b0fc/ray-2.51.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:0bed9408712bad1511e65683a455302f88d94e5e5cb6a58cc4a154b61d8a0b4a", size = 70502423, upload-time = "2025-11-01T03:24:27.398Z" }, - { url = "https://files.pythonhosted.org/packages/ee/59/69b7a653ed8176fc7fd894d462ed34bb1477e7fa71700324de99179b5b7e/ray-2.51.1-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:4e786da7862cf73664977d0212a505d6d5a585beadf63e7dc1e1c129259bee20", size = 71353730, upload-time = "2025-11-01T03:24:33.495Z" }, - { url = "https://files.pythonhosted.org/packages/38/91/0c4fe7aed34baa14d9c050c88f39ff16083d555bd6dcd6c4ffb4332a6f8a/ray-2.51.1-cp312-cp312-win_amd64.whl", hash = "sha256:198fda93074a6863555f4003e9013bb2ba0cd50b59b18c02affdc294b28a2eef", size = 26674921, upload-time = "2025-11-01T03:24:38.394Z" }, + { url = "https://files.pythonhosted.org/packages/38/68/8e59b8413f3751fe7ce8b98ee8787d13964b47a4043587950790a9dd2151/ray-2.53.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:65e2ce58d3dc6baa3cf45824d889c1968ebde565ee54dfd80a98af8f31af8e4a", size = 71504450, upload-time = "2025-12-20T16:06:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/2a/db/978a50d264565ca42e2a4bf115ec9a1f04f19ca5e620e6aa2f280747b644/ray-2.53.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:14f46363e9b4cf0c1c8b4d8623ec337c5bd408377831b5e5b50067930137bbca", size = 72370424, upload-time = "2025-12-20T16:06:40.821Z" }, ] [package.optional-dependencies] cgraph = [ - { name = "cupy-cuda12x", marker = "sys_platform != 'darwin'" }, -] - -[[package]] -name = "redis" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, + { name = "cupy-cuda12x", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -2114,9 +2073,9 @@ name = "referencing" version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions" }, + { name = "attrs", marker = "sys_platform == 'linux'" }, + { name = "rpds-py", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] 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 = [ @@ -2175,16 +2134,16 @@ wheels = [ [[package]] name = "rich-toolkit" -version = "0.15.1" +version = "0.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "typing-extensions" }, + { name = "click", marker = "sys_platform == 'linux'" }, + { name = "rich", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/33/1a18839aaa8feef7983590c05c22c9c09d245ada6017d118325bbfcc7651/rich_toolkit-0.15.1.tar.gz", hash = "sha256:6f9630eb29f3843d19d48c3bd5706a086d36d62016687f9d0efa027ddc2dd08a", size = 115322, upload-time = "2025-09-04T09:28:11.789Z" } +sdist = { url = "https://files.pythonhosted.org/packages/97/09/3f9b8d9daaf235195c626f21e03604c05b987404ee3bcacee0c1f67f2a8e/rich_toolkit-0.17.1.tar.gz", hash = "sha256:5af54df8d1dd9c8530e462e1bdcaed625c9b49f5a55b035aa0ba1c17bdb87c9a", size = 187925, upload-time = "2025-12-17T10:49:22.583Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/49/42821d55ead7b5a87c8d121edf323cb393d8579f63e933002ade900b784f/rich_toolkit-0.15.1-py3-none-any.whl", hash = "sha256:36a0b1d9a135d26776e4b78f1d5c2655da6e0ef432380b5c6b523c8d8ab97478", size = 29412, upload-time = "2025-09-04T09:28:10.587Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7b/15e55fa8a76d0d41bf34d965af78acdaf80a315907adb30de8b63c272694/rich_toolkit-0.17.1-py3-none-any.whl", hash = "sha256:96d24bb921ecd225ffce7c526a9149e74006410c05e6d405bd74ffd54d5631ed", size = 31412, upload-time = "2025-12-17T10:49:21.793Z" }, ] [[package]] @@ -2193,8 +2152,6 @@ version = "0.7.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/0e/012556ef3047a2628842b44e753bb15f4dc46806780ff090f1e8fe4bf1eb/rignore-0.7.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", size = 883488, upload-time = "2025-11-05T20:42:41.359Z" }, - { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, @@ -2205,80 +2162,72 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/c7/28/fa5dcd1e2e16982c359128664e3785f202d3eca9b22dd0b2f91c4b3d242f/rignore-0.7.6-cp312-cp312-win32.whl", hash = "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", size = 646145, upload-time = "2025-11-05T21:41:51.096Z" }, - { url = "https://files.pythonhosted.org/packages/26/87/69387fb5dd81a0f771936381431780b8cf66fcd2cfe9495e1aaf41548931/rignore-0.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", size = 726090, upload-time = "2025-11-05T21:41:36.485Z" }, - { url = "https://files.pythonhosted.org/packages/24/5f/e8418108dcda8087fb198a6f81caadbcda9fd115d61154bf0df4d6d3619b/rignore-0.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", size = 656317, upload-time = "2025-11-05T21:41:25.305Z" }, ] [[package]] name = "rpds-py" -version = "0.28.0" +version = "0.30.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419, upload-time = "2025-10-22T22:24:29.327Z" } +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/b8/5c/6c3936495003875fe7b14f90ea812841a08fca50ab26bd840e924097d9c8/rpds_py-0.28.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b4f28583a4f247ff60cd7bdda83db8c3f5b05a7a82ff20dd4b078571747708f", size = 366439, upload-time = "2025-10-22T22:22:04.525Z" }, - { url = "https://files.pythonhosted.org/packages/56/f9/a0f1ca194c50aa29895b442771f036a25b6c41a35e4f35b1a0ea713bedae/rpds_py-0.28.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424", size = 348170, upload-time = "2025-10-22T22:22:06.397Z" }, - { url = "https://files.pythonhosted.org/packages/18/ea/42d243d3a586beb72c77fa5def0487daf827210069a95f36328e869599ea/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628", size = 378838, upload-time = "2025-10-22T22:22:07.932Z" }, - { url = "https://files.pythonhosted.org/packages/e7/78/3de32e18a94791af8f33601402d9d4f39613136398658412a4e0b3047327/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd", size = 393299, upload-time = "2025-10-22T22:22:09.435Z" }, - { url = "https://files.pythonhosted.org/packages/13/7e/4bdb435afb18acea2eb8a25ad56b956f28de7c59f8a1d32827effa0d4514/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e", size = 518000, upload-time = "2025-10-22T22:22:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/31/d0/5f52a656875cdc60498ab035a7a0ac8f399890cc1ee73ebd567bac4e39ae/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a", size = 408746, upload-time = "2025-10-22T22:22:13.143Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cd/49ce51767b879cde77e7ad9fae164ea15dce3616fe591d9ea1df51152706/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84", size = 386379, upload-time = "2025-10-22T22:22:14.602Z" }, - { url = "https://files.pythonhosted.org/packages/6a/99/e4e1e1ee93a98f72fc450e36c0e4d99c35370220e815288e3ecd2ec36a2a/rpds_py-0.28.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66", size = 401280, upload-time = "2025-10-22T22:22:16.063Z" }, - { url = "https://files.pythonhosted.org/packages/61/35/e0c6a57488392a8b319d2200d03dad2b29c0db9996f5662c3b02d0b86c02/rpds_py-0.28.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5deca01b271492553fdb6c7fd974659dce736a15bae5dad7ab8b93555bceb28", size = 412365, upload-time = "2025-10-22T22:22:17.504Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6a/841337980ea253ec797eb084665436007a1aad0faac1ba097fb906c5f69c/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:735f8495a13159ce6a0d533f01e8674cec0c57038c920495f87dcb20b3ddb48a", size = 559573, upload-time = "2025-10-22T22:22:19.108Z" }, - { url = "https://files.pythonhosted.org/packages/e7/5e/64826ec58afd4c489731f8b00729c5f6afdb86f1df1df60bfede55d650bb/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:961ca621ff10d198bbe6ba4957decca61aa2a0c56695384c1d6b79bf61436df5", size = 583973, upload-time = "2025-10-22T22:22:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ee/44d024b4843f8386a4eeaa4c171b3d31d55f7177c415545fd1a24c249b5d/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2374e16cc9131022e7d9a8f8d65d261d9ba55048c78f3b6e017971a4f5e6353c", size = 553800, upload-time = "2025-10-22T22:22:22.25Z" }, - { url = "https://files.pythonhosted.org/packages/7d/89/33e675dccff11a06d4d85dbb4d1865f878d5020cbb69b2c1e7b2d3f82562/rpds_py-0.28.0-cp312-cp312-win32.whl", hash = "sha256:d15431e334fba488b081d47f30f091e5d03c18527c325386091f31718952fe08", size = 216954, upload-time = "2025-10-22T22:22:24.105Z" }, - { url = "https://files.pythonhosted.org/packages/af/36/45f6ebb3210887e8ee6dbf1bc710ae8400bb417ce165aaf3024b8360d999/rpds_py-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:a410542d61fc54710f750d3764380b53bf09e8c4edbf2f9141a82aa774a04f7c", size = 227844, upload-time = "2025-10-22T22:22:25.551Z" }, - { url = "https://files.pythonhosted.org/packages/57/91/f3fb250d7e73de71080f9a221d19bd6a1c1eb0d12a1ea26513f6c1052ad6/rpds_py-0.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:1f0cfd1c69e2d14f8c892b893997fa9a60d890a0c8a603e88dca4955f26d1edd", size = 217624, upload-time = "2025-10-22T22:22:26.914Z" }, + { 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" }, ] [[package]] name = "ruff" -version = "0.14.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/fa/fbb67a5780ae0f704876cb8ac92d6d76da41da4dc72b7ed3565ab18f2f52/ruff-0.14.5.tar.gz", hash = "sha256:8d3b48d7d8aad423d3137af7ab6c8b1e38e4de104800f0d596990f6ada1a9fc1", size = 5615944, upload-time = "2025-11-13T19:58:51.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/31/c07e9c535248d10836a94e4f4e8c5a31a1beed6f169b31405b227872d4f4/ruff-0.14.5-py3-none-linux_armv6l.whl", hash = "sha256:f3b8248123b586de44a8018bcc9fefe31d23dda57a34e6f0e1e53bd51fd63594", size = 13171630, upload-time = "2025-11-13T19:57:54.894Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/283c62516dca697cd604c2796d1487396b7a436b2f0ecc3fd412aca470e0/ruff-0.14.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f7a75236570318c7a30edd7f5491945f0169de738d945ca8784500b517163a72", size = 13413925, upload-time = "2025-11-13T19:57:59.181Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f3/aa319f4afc22cb6fcba2b9cdfc0f03bbf747e59ab7a8c5e90173857a1361/ruff-0.14.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d146132d1ee115f8802356a2dc9a634dbf58184c51bff21f313e8cd1c74899a", size = 12574040, upload-time = "2025-11-13T19:58:02.056Z" }, - { url = "https://files.pythonhosted.org/packages/f9/7f/cb5845fcc7c7e88ed57f58670189fc2ff517fe2134c3821e77e29fd3b0c8/ruff-0.14.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2380596653dcd20b057794d55681571a257a42327da8894b93bbd6111aa801f", size = 13009755, upload-time = "2025-11-13T19:58:05.172Z" }, - { url = "https://files.pythonhosted.org/packages/21/d2/bcbedbb6bcb9253085981730687ddc0cc7b2e18e8dc13cf4453de905d7a0/ruff-0.14.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68", size = 12937641, upload-time = "2025-11-13T19:58:08.345Z" }, - { url = "https://files.pythonhosted.org/packages/a4/58/e25de28a572bdd60ffc6bb71fc7fd25a94ec6a076942e372437649cbb02a/ruff-0.14.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88f0770d42b7fa02bbefddde15d235ca3aa24e2f0137388cc15b2dcbb1f7c7a7", size = 13610854, upload-time = "2025-11-13T19:58:11.419Z" }, - { url = "https://files.pythonhosted.org/packages/7d/24/43bb3fd23ecee9861970978ea1a7a63e12a204d319248a7e8af539984280/ruff-0.14.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78", size = 15061088, upload-time = "2025-11-13T19:58:14.551Z" }, - { url = "https://files.pythonhosted.org/packages/23/44/a022f288d61c2f8c8645b24c364b719aee293ffc7d633a2ca4d116b9c716/ruff-0.14.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b595bedf6bc9cab647c4a173a61acf4f1ac5f2b545203ba82f30fcb10b0318fb", size = 14734717, upload-time = "2025-11-13T19:58:17.518Z" }, - { url = "https://files.pythonhosted.org/packages/58/81/5c6ba44de7e44c91f68073e0658109d8373b0590940efe5bd7753a2585a3/ruff-0.14.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f55382725ad0bdb2e8ee2babcbbfb16f124f5a59496a2f6a46f1d9d99d93e6e2", size = 14028812, upload-time = "2025-11-13T19:58:20.533Z" }, - { url = "https://files.pythonhosted.org/packages/ad/ef/41a8b60f8462cb320f68615b00299ebb12660097c952c600c762078420f8/ruff-0.14.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7497d19dce23976bdaca24345ae131a1d38dcfe1b0850ad8e9e6e4fa321a6e19", size = 13825656, upload-time = "2025-11-13T19:58:23.345Z" }, - { url = "https://files.pythonhosted.org/packages/7c/00/207e5de737fdb59b39eb1fac806904fe05681981b46d6a6db9468501062e/ruff-0.14.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:410e781f1122d6be4f446981dd479470af86537fb0b8857f27a6e872f65a38e4", size = 13959922, upload-time = "2025-11-13T19:58:26.537Z" }, - { url = "https://files.pythonhosted.org/packages/bc/7e/fa1f5c2776db4be405040293618846a2dece5c70b050874c2d1f10f24776/ruff-0.14.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01be527ef4c91a6d55e53b337bfe2c0f82af024cc1a33c44792d6844e2331e1", size = 12932501, upload-time = "2025-11-13T19:58:29.822Z" }, - { url = "https://files.pythonhosted.org/packages/67/d8/d86bf784d693a764b59479a6bbdc9515ae42c340a5dc5ab1dabef847bfaa/ruff-0.14.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f66e9bb762e68d66e48550b59c74314168ebb46199886c5c5aa0b0fbcc81b151", size = 12927319, upload-time = "2025-11-13T19:58:32.923Z" }, - { url = "https://files.pythonhosted.org/packages/ac/de/ee0b304d450ae007ce0cb3e455fe24fbcaaedae4ebaad6c23831c6663651/ruff-0.14.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d93be8f1fa01022337f1f8f3bcaa7ffee2d0b03f00922c45c2207954f351f465", size = 13206209, upload-time = "2025-11-13T19:58:35.952Z" }, - { url = "https://files.pythonhosted.org/packages/33/aa/193ca7e3a92d74f17d9d5771a765965d2cf42c86e6f0fd95b13969115723/ruff-0.14.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c135d4b681f7401fe0e7312017e41aba9b3160861105726b76cfa14bc25aa367", size = 13953709, upload-time = "2025-11-13T19:58:39.002Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f1/7119e42aa1d3bf036ffc9478885c2e248812b7de9abea4eae89163d2929d/ruff-0.14.5-py3-none-win32.whl", hash = "sha256:c83642e6fccfb6dea8b785eb9f456800dcd6a63f362238af5fc0c83d027dd08b", size = 12925808, upload-time = "2025-11-13T19:58:42.779Z" }, - { url = "https://files.pythonhosted.org/packages/3b/9d/7c0a255d21e0912114784e4a96bf62af0618e2190cae468cd82b13625ad2/ruff-0.14.5-py3-none-win_amd64.whl", hash = "sha256:9d55d7af7166f143c94eae1db3312f9ea8f95a4defef1979ed516dbb38c27621", size = 14331546, upload-time = "2025-11-13T19:58:45.691Z" }, - { url = "https://files.pythonhosted.org/packages/e5/80/69756670caedcf3b9be597a6e12276a6cf6197076eb62aad0c608f8efce0/ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4", size = 13433331, upload-time = "2025-11-13T19:58:48.434Z" }, +version = "0.14.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, ] [[package]] name = "safetensors" -version = "0.6.2" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/cc/738f3011628920e027a11754d9cae9abec1aed00f7ae860abbf843755233/safetensors-0.6.2.tar.gz", hash = "sha256:43ff2aa0e6fa2dc3ea5524ac7ad93a9839256b8703761e76e2d0b2a3fa4f15d9", size = 197968, upload-time = "2025-08-08T13:13:58.654Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/b1/3f5fd73c039fc87dba3ff8b5d528bfc5a32b597fea8e7a6a4800343a17c7/safetensors-0.6.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9c85ede8ec58f120bad982ec47746981e210492a6db876882aa021446af8ffba", size = 454797, upload-time = "2025-08-08T13:13:52.066Z" }, - { url = "https://files.pythonhosted.org/packages/8c/c9/bb114c158540ee17907ec470d01980957fdaf87b4aa07914c24eba87b9c6/safetensors-0.6.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d6675cf4b39c98dbd7d940598028f3742e0375a6b4d4277e76beb0c35f4b843b", size = 432206, upload-time = "2025-08-08T13:13:50.931Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8e/f70c34e47df3110e8e0bb268d90db8d4be8958a54ab0336c9be4fe86dac8/safetensors-0.6.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d2d2b3ce1e2509c68932ca03ab8f20570920cd9754b05063d4368ee52833ecd", size = 473261, upload-time = "2025-08-08T13:13:41.259Z" }, - { url = "https://files.pythonhosted.org/packages/2a/f5/be9c6a7c7ef773e1996dc214e73485286df1836dbd063e8085ee1976f9cb/safetensors-0.6.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:93de35a18f46b0f5a6a1f9e26d91b442094f2df02e9fd7acf224cfec4238821a", size = 485117, upload-time = "2025-08-08T13:13:43.506Z" }, - { url = "https://files.pythonhosted.org/packages/c9/55/23f2d0a2c96ed8665bf17a30ab4ce5270413f4d74b6d87dd663258b9af31/safetensors-0.6.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89a89b505f335640f9120fac65ddeb83e40f1fd081cb8ed88b505bdccec8d0a1", size = 616154, upload-time = "2025-08-08T13:13:45.096Z" }, - { url = "https://files.pythonhosted.org/packages/98/c6/affb0bd9ce02aa46e7acddbe087912a04d953d7a4d74b708c91b5806ef3f/safetensors-0.6.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4d0d0b937e04bdf2ae6f70cd3ad51328635fe0e6214aa1fc811f3b576b3bda", size = 520713, upload-time = "2025-08-08T13:13:46.25Z" }, - { url = "https://files.pythonhosted.org/packages/fe/5d/5a514d7b88e310c8b146e2404e0dc161282e78634d9358975fd56dfd14be/safetensors-0.6.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8045db2c872db8f4cbe3faa0495932d89c38c899c603f21e9b6486951a5ecb8f", size = 485835, upload-time = "2025-08-08T13:13:49.373Z" }, - { url = "https://files.pythonhosted.org/packages/7a/7b/4fc3b2ba62c352b2071bea9cfbad330fadda70579f617506ae1a2f129cab/safetensors-0.6.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:81e67e8bab9878bb568cffbc5f5e655adb38d2418351dc0859ccac158f753e19", size = 521503, upload-time = "2025-08-08T13:13:47.651Z" }, - { url = "https://files.pythonhosted.org/packages/5a/50/0057e11fe1f3cead9254315a6c106a16dd4b1a19cd247f7cc6414f6b7866/safetensors-0.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0e4d029ab0a0e0e4fdf142b194514695b1d7d3735503ba700cf36d0fc7136ce", size = 652256, upload-time = "2025-08-08T13:13:53.167Z" }, - { url = "https://files.pythonhosted.org/packages/e9/29/473f789e4ac242593ac1656fbece6e1ecd860bb289e635e963667807afe3/safetensors-0.6.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fa48268185c52bfe8771e46325a1e21d317207bcabcb72e65c6e28e9ffeb29c7", size = 747281, upload-time = "2025-08-08T13:13:54.656Z" }, - { url = "https://files.pythonhosted.org/packages/68/52/f7324aad7f2df99e05525c84d352dc217e0fa637a4f603e9f2eedfbe2c67/safetensors-0.6.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:d83c20c12c2d2f465997c51b7ecb00e407e5f94d7dec3ea0cc11d86f60d3fde5", size = 692286, upload-time = "2025-08-08T13:13:55.884Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fe/cad1d9762868c7c5dc70c8620074df28ebb1a8e4c17d4c0cb031889c457e/safetensors-0.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d944cea65fad0ead848b6ec2c37cc0b197194bec228f8020054742190e9312ac", size = 655957, upload-time = "2025-08-08T13:13:57.029Z" }, - { url = "https://files.pythonhosted.org/packages/59/a7/e2158e17bbe57d104f0abbd95dff60dda916cf277c9f9663b4bf9bad8b6e/safetensors-0.6.2-cp38-abi3-win32.whl", hash = "sha256:cab75ca7c064d3911411461151cb69380c9225798a20e712b102edda2542ddb1", size = 308926, upload-time = "2025-08-08T13:14:01.095Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c3/c0be1135726618dc1e28d181b8c442403d8dbb9e273fd791de2d4384bcdd/safetensors-0.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:c7b214870df923cbc1593c3faee16bec59ea462758699bd3fee399d00aac072c", size = 320192, upload-time = "2025-08-08T13:13:59.467Z" }, + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, ] [[package]] @@ -2286,20 +2235,14 @@ name = "scipy" version = "1.16.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, - { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, - { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, - { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, - { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, ] [[package]] @@ -2308,27 +2251,21 @@ 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" }, ] [[package]] name = "sentry-sdk" -version = "2.43.0" +version = "2.48.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/18/09875b4323b03ca9025bae7e6539797b27e4fc032998a466b4b9c3d24653/sentry_sdk-2.43.0.tar.gz", hash = "sha256:52ed6e251c5d2c084224d73efee56b007ef5c2d408a4a071270e82131d336e20", size = 368953, upload-time = "2025-10-29T11:26:08.156Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/f0/0e9dc590513d5e742d7799e2038df3a05167cba084c6ca4f3cdd75b55164/sentry_sdk-2.48.0.tar.gz", hash = "sha256:5213190977ff7fdff8a58b722fb807f8d5524a80488626ebeda1b5676c0c1473", size = 384828, upload-time = "2025-12-16T14:55:41.722Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/31/8228fa962f7fd8814d634e4ebece8780e2cdcfbdf0cd2e14d4a6861a7cd5/sentry_sdk-2.43.0-py2.py3-none-any.whl", hash = "sha256:4aacafcf1756ef066d359ae35030881917160ba7f6fc3ae11e0e58b09edc2d5d", size = 400997, upload-time = "2025-10-29T11:26:05.77Z" }, + { url = "https://files.pythonhosted.org/packages/4d/19/8d77f9992e5cbfcaa9133c3bf63b4fbbb051248802e1e803fed5c552fbb2/sentry_sdk-2.48.0-py2.py3-none-any.whl", hash = "sha256:6b12ac256769d41825d9b7518444e57fa35b5642df4c7c5e322af4d2c8721172", size = 414555, upload-time = "2025-12-16T14:55:40.152Z" }, ] [[package]] @@ -2337,25 +2274,21 @@ version = "1.3.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/f0/2dc88e842077719d7384d86cc47403e5102810492b33680e7dadcee64cd8/setproctitle-1.3.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2dc99aec591ab6126e636b11035a70991bc1ab7a261da428491a40b84376654e", size = 18049, upload-time = "2025-09-05T12:49:36.241Z" }, - { url = "https://files.pythonhosted.org/packages/f0/b4/50940504466689cda65680c9e9a1e518e5750c10490639fa687489ac7013/setproctitle-1.3.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdd8aa571b7aa39840fdbea620e308a19691ff595c3a10231e9ee830339dd798", size = 13079, upload-time = "2025-09-05T12:49:38.088Z" }, { url = "https://files.pythonhosted.org/packages/d0/99/71630546b9395b095f4082be41165d1078204d1696c2d9baade3de3202d0/setproctitle-1.3.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2906b6c7959cdb75f46159bf0acd8cc9906cf1361c9e1ded0d065fe8f9039629", size = 32932, upload-time = "2025-09-05T12:49:39.271Z" }, { url = "https://files.pythonhosted.org/packages/50/22/cee06af4ffcfb0e8aba047bd44f5262e644199ae7527ae2c1f672b86495c/setproctitle-1.3.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6915964a6dda07920a1159321dcd6d94fc7fc526f815ca08a8063aeca3c204f1", size = 33736, upload-time = "2025-09-05T12:49:40.565Z" }, { url = "https://files.pythonhosted.org/packages/5c/00/a5949a8bb06ef5e7df214fc393bb2fb6aedf0479b17214e57750dfdd0f24/setproctitle-1.3.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cff72899861c765bd4021d1ff1c68d60edc129711a2fdba77f9cb69ef726a8b6", size = 35605, upload-time = "2025-09-05T12:49:42.362Z" }, { url = "https://files.pythonhosted.org/packages/b0/3a/50caca532a9343828e3bf5778c7a84d6c737a249b1796d50dd680290594d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7cb05bd446687ff816a3aaaf831047fc4c364feff7ada94a66024f1367b448c", size = 33143, upload-time = "2025-09-05T12:49:43.515Z" }, { url = "https://files.pythonhosted.org/packages/ca/14/b843a251296ce55e2e17c017d6b9f11ce0d3d070e9265de4ecad948b913d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3a57b9a00de8cae7e2a1f7b9f0c2ac7b69372159e16a7708aa2f38f9e5cc987a", size = 34434, upload-time = "2025-09-05T12:49:45.31Z" }, { url = "https://files.pythonhosted.org/packages/c8/b7/06145c238c0a6d2c4bc881f8be230bb9f36d2bf51aff7bddcb796d5eed67/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d8828b356114f6b308b04afe398ed93803d7fca4a955dd3abe84430e28d33739", size = 32795, upload-time = "2025-09-05T12:49:46.419Z" }, - { url = "https://files.pythonhosted.org/packages/ef/dc/ef76a81fac9bf27b84ed23df19c1f67391a753eed6e3c2254ebcb5133f56/setproctitle-1.3.7-cp312-cp312-win32.whl", hash = "sha256:b0304f905efc845829ac2bc791ddebb976db2885f6171f4a3de678d7ee3f7c9f", size = 12552, upload-time = "2025-09-05T12:49:47.635Z" }, - { url = "https://files.pythonhosted.org/packages/e2/5b/a9fe517912cd6e28cf43a212b80cb679ff179a91b623138a99796d7d18a0/setproctitle-1.3.7-cp312-cp312-win_amd64.whl", hash = "sha256:9888ceb4faea3116cf02a920ff00bfbc8cc899743e4b4ac914b03625bdc3c300", size = 13247, upload-time = "2025-09-05T12:49:49.16Z" }, ] [[package]] name = "setuptools" -version = "79.0.1" +version = "80.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/71/b6365e6325b3290e14957b2c3a804a529968c77a049b2ed40c095f749707/setuptools-79.0.1.tar.gz", hash = "sha256:128ce7b8f33c3079fd1b067ecbb4051a66e8526e7b65f6cec075dfc650ddfa88", size = 1367909, upload-time = "2025-04-23T22:20:59.241Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/6d/b4752b044bf94cb802d88a888dc7d288baaf77d7910b7dedda74b5ceea0c/setuptools-79.0.1-py3-none-any.whl", hash = "sha256:e147c0549f27767ba362f9da434eab9c5dc0045d5304feb602a0af001089fc51", size = 1256281, upload-time = "2025-04-23T22:20:56.768Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] [[package]] @@ -2376,6 +2309,15 @@ 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 = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -2387,28 +2329,28 @@ wheels = [ [[package]] name = "sse-starlette" -version = "3.0.4" +version = "3.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "starlette" }, + { name = "anyio", marker = "sys_platform == 'linux'" }, + { name = "starlette", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/8b/54651ad49bce99a50fd61a7f19c2b6a79fbb072e693101fbb1194c362054/sse_starlette-3.0.4.tar.gz", hash = "sha256:5e34286862e96ead0eb70f5ddd0bd21ab1f6473a8f44419dd267f431611383dd", size = 22576, upload-time = "2025-12-14T16:22:52.493Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/08/8f554b0e5bad3e4e880521a1686d96c05198471eed860b0eb89b57ea3636/sse_starlette-3.1.1.tar.gz", hash = "sha256:bffa531420c1793ab224f63648c059bcadc412bf9fdb1301ac8de1cf9a67b7fb", size = 24306, upload-time = "2025-12-26T15:22:53.836Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/22/8ab1066358601163e1ac732837adba3672f703818f693e179b24e0d3b65c/sse_starlette-3.0.4-py3-none-any.whl", hash = "sha256:32c80ef0d04506ced4b0b6ab8fe300925edc37d26f666afb1874c754895f5dc3", size = 11764, upload-time = "2025-12-14T16:22:51.453Z" }, + { url = "https://files.pythonhosted.org/packages/e3/31/4c281581a0f8de137b710a07f65518b34bcf333b201cfa06cfda9af05f8a/sse_starlette-3.1.1-py3-none-any.whl", hash = "sha256:bb38f71ae74cfd86b529907a9fda5632195dfa6ae120f214ea4c890c7ee9d436", size = 12442, upload-time = "2025-12-26T15:22:52.911Z" }, ] [[package]] name = "starlette" -version = "0.49.3" +version = "0.50.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "typing-extensions" }, + { name = "anyio", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031, upload-time = "2025-11-01T15:12:26.13Z" } +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 = [ - { url = "https://files.pythonhosted.org/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340, upload-time = "2025-11-01T15:12:24.387Z" }, + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, ] [[package]] @@ -2446,18 +2388,15 @@ name = "tiktoken" version = "0.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "regex" }, - { name = "requests" }, + { name = "regex", marker = "sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, - { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, - { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, ] [[package]] @@ -2487,38 +2426,97 @@ wheels = [ [[package]] name = "torch" -version = "2.9.0" +version = "2.9.0+cu128" +source = { registry = "https://download.pytorch.org/whl/cu128" } +resolution-markers = [ + "sys_platform == 'linux'", +] +dependencies = [ + { name = "filelock", marker = "sys_platform == 'linux'" }, + { name = "fsspec", marker = "sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'linux'" }, + { name = "networkx", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "sys_platform == 'linux'" }, + { name = "setuptools", marker = "sys_platform == 'linux'" }, + { name = "sympy", marker = "sys_platform == 'linux'" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://download.pytorch.org/whl/cu128/torch-2.9.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e1765625084e320f1eb2f4eb5fd9d14d39d08d7a1880c10a307ce5de20831d27" }, + { url = "https://download.pytorch.org/whl/cu128/torch-2.9.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:87c62d3b95f1a2270bd116dbd47dc515c0b2035076fbb4a03b4365ea289e89c4" }, +] + +[[package]] +name = "torch" +version = "2.9.1" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "sys_platform == 'darwin'", +] +dependencies = [ + { name = "filelock", marker = "sys_platform == 'darwin'" }, + { name = "fsspec", marker = "sys_platform == 'darwin'" }, + { name = "jinja2", marker = "sys_platform == 'darwin'" }, + { name = "networkx", marker = "sys_platform == 'darwin'" }, + { name = "setuptools", marker = "sys_platform == 'darwin'" }, + { name = "sympy", marker = "sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp312-none-macosx_11_0_arm64.whl" }, +] + +[[package]] +name = "torch" +version = "2.9.1+cpu" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "sys_platform != 'darwin' and sys_platform != 'linux'", +] +dependencies = [ + { name = "filelock", marker = "sys_platform != 'darwin' and sys_platform != 'linux'" }, + { name = "fsspec", marker = "sys_platform != 'darwin' and sys_platform != 'linux'" }, + { name = "jinja2", marker = "sys_platform != 'darwin' and sys_platform != 'linux'" }, + { name = "networkx", marker = "sys_platform != 'darwin' and sys_platform != 'linux'" }, + { name = "setuptools", marker = "sys_platform != 'darwin' and sys_platform != 'linux'" }, + { name = "sympy", marker = "sys_platform != 'darwin' and sys_platform != 'linux'" }, + { name = "typing-extensions", marker = "sys_platform != 'darwin' and sys_platform != 'linux'" }, +] +wheels = [ + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp312-cp312-win_amd64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp312-cp312-win_arm64.whl" }, +] + +[[package]] +name = "torch-c-dlpack-ext" +version = "0.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "jinja2" }, - { name = "networkx" }, - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "setuptools" }, - { name = "sympy" }, - { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions" }, + { name = "torch", version = "2.9.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.9.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { name = "torch", version = "2.9.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin' and sys_platform != 'linux'" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/b7/cc/24e5eee56bfe2f99b9c026d55bc1a77ceaf409791d9be71a001ede1b2f4e/torch_c_dlpack_ext-0.1.4.tar.gz", hash = "sha256:ad292d17e285ab9523940e51e87d21ffce4982ce8beb46fb18b5c2b4760a1a10", size = 3683, upload-time = "2025-12-09T00:37:56.739Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d3/3985739f3b8e88675127bf70f82b3a48ae083e39cda56305dbd90398fec0/torch-2.9.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e5f7af1dc4c0a7c4a260c2534f41ddaf209714f7c89145e644c44712fbd6b642", size = 104107898, upload-time = "2025-10-15T15:46:20.883Z" }, - { url = "https://files.pythonhosted.org/packages/a5/4b/f4bb2e6c25d0272f798cd6d7a04ed315da76cec68c602d87040c7847287f/torch-2.9.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:01cff95ecd9a212ea2f141db28acccdceb6a4c54f64e6c51091146f5e2a772c6", size = 899738273, upload-time = "2025-10-15T15:50:04.188Z" }, - { url = "https://files.pythonhosted.org/packages/66/11/c1c5ba6691cda6279087c35bd626536e4fd29521fe740abf5008377a9a02/torch-2.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4582b162f541651f0cb184d3e291c05c2f556c7117c64a9873e2ee158d40062b", size = 109280887, upload-time = "2025-10-15T15:46:26.228Z" }, - { url = "https://files.pythonhosted.org/packages/dd/5f/b85bd8c05312d71de9402bf5868d217c38827cfd09d8f8514e5be128a52b/torch-2.9.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:33f58e9a102a91259af289d50525c30323b5c9ae1d31322b6447c0814da68695", size = 74478983, upload-time = "2025-10-15T15:46:39.406Z" }, + { url = "https://files.pythonhosted.org/packages/84/c8/97c3d4a1c05dd41e4ba70a8abff47a93951c035a3db1a532777b372f63bc/torch_c_dlpack_ext-0.1.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78253bc0d2ee4f0c4bf38e207f19de93ba3625430e5ecb08f3a800d93ea9a144", size = 5281944, upload-time = "2025-12-09T00:37:32.153Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4c/7d59344006807613baca1daf3109ea6a81743146330b5afde96dc953115c/torch_c_dlpack_ext-0.1.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b3f6beab017961a082f4012a68fcdeddb55a8b10cd8db630902bd46068b5e5", size = 433744, upload-time = "2025-12-09T00:37:33.538Z" }, + { url = "https://files.pythonhosted.org/packages/35/c0/0ae9067fd9f15f1feacfd7398314aa48f922ddb9cbeb5f95c8a2e1831cb8/torch_c_dlpack_ext-0.1.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:383794a3281862f8736efa99789713187fb8e1937a5e2f32456bbbe52fa3a8a3", size = 888525, upload-time = "2025-12-09T00:37:35.374Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5c/74143cedeaf98f632b60c68770e697ce978f75ff6de8f3fe6e58f46459ba/torch_c_dlpack_ext-0.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:d4a343642c8ee46f1257731b0a5473a76eacaf1a09db13dbf12a2b012b586041", size = 1473667, upload-time = "2025-12-09T00:37:37.382Z" }, ] [[package]] @@ -2526,13 +2524,11 @@ name = "torchaudio" version = "2.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "torch" }, + { name = "torch", version = "2.9.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/63/3c0ede3aa3d19a8a6698ddd107fa88660549360b51bf8ce2717cd498d800/torchaudio-2.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab4cbcccfd873b0fb41fcb39c9869e59ef84bb95b093f6f58e2d05172a7500d2", size = 809116, upload-time = "2025-10-15T15:52:00.911Z" }, { url = "https://files.pythonhosted.org/packages/be/d5/25e58745defe9d05893d3cba5c0e1a76aeaac503ac5ec4d9f83c871df71c/torchaudio-2.9.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:7f93388b6e536c14d6015b6f75277a8b45efc532f61b35adc1ed06c98a86003e", size = 476020, upload-time = "2025-10-15T15:51:59.967Z" }, { url = "https://files.pythonhosted.org/packages/f0/9c/58b8b49dfba2ae85e41ca86b0c52de45bbbea01987490de219c99c523a58/torchaudio-2.9.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:508318a2130b40ad51378f90caf8727a4bd3ac2b296f2b90c900b44e6068a940", size = 2059901, upload-time = "2025-10-15T15:51:54.634Z" }, - { url = "https://files.pythonhosted.org/packages/d7/eb/58b05f75d12f69ccc460893a20c999da082e063082120ed06e05cca3a053/torchaudio-2.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:82117e3a605f2959dc09b4cd8a11178d6e92727d5f85e5d4f9fe47502f84ee96", size = 665350, upload-time = "2025-10-15T15:52:08.384Z" }, ] [[package]] @@ -2540,15 +2536,13 @@ name = "torchvision" version = "0.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "pillow" }, - { name = "torch" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, + { name = "pillow", marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.9.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/47/ef/81e4e69e02e2c4650b30e8c11c8974f946682a30e0ab7e9803a831beff76/torchvision-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c61d40bcd2e2451e932902a702ad495ba1ec6f279e90b1e15cef2bb55dc911e2", size = 1891726, upload-time = "2025-10-15T15:51:16.977Z" }, { url = "https://files.pythonhosted.org/packages/00/7b/e3809b3302caea9a12c13f3adebe4fef127188438e719fd6c8dc93db1da6/torchvision-0.24.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b0531d1483fc322d7da0d83be52f0df860a75114ab87dbeeb9de765feaeda843", size = 2419495, upload-time = "2025-10-15T15:51:11.885Z" }, { url = "https://files.pythonhosted.org/packages/7e/e6/7324ead6793075a8c75c56abeed1236d1750de16a5613cfe2ddad164a92a/torchvision-0.24.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:26b9dd9c083f8e5f7ac827de6d5b88c615d9c582dc87666770fbdf16887e4c25", size = 8050480, upload-time = "2025-10-15T15:51:24.012Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ad/3c56fcd2a0d6e8afa80e115b5ade4302232ec99655220a51d05709819523/torchvision-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:060b7c50ed4b3fb0316b08e2e31bfd874ec2f63ef5ae02f81e54341ca4e88703", size = 4292225, upload-time = "2025-10-15T15:51:27.699Z" }, ] [[package]] @@ -2565,12 +2559,13 @@ wheels = [ [[package]] name = "transformers" -version = "4.57.1" +version = "4.57.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "huggingface-hub" }, - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, + { name = "numpy", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, { name = "packaging" }, { name = "pyyaml" }, { name = "regex" }, @@ -2579,9 +2574,9 @@ dependencies = [ { name = "tokenizers" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/68/a39307bcc4116a30b2106f2e689130a48de8bd8a1e635b5e1030e46fcd9e/transformers-4.57.1.tar.gz", hash = "sha256:f06c837959196c75039809636cd964b959f6604b75b8eeec6fdfc0440b89cc55", size = 10142511, upload-time = "2025-10-14T15:39:26.18Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/70/d42a739e8dfde3d92bb2fff5819cbf331fe9657323221e79415cd5eb65ee/transformers-4.57.3.tar.gz", hash = "sha256:df4945029aaddd7c09eec5cad851f30662f8bd1746721b34cc031d70c65afebc", size = 10139680, upload-time = "2025-11-25T15:51:30.139Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/d3/c16c3b3cf7655a67db1144da94b021c200ac1303f82428f2beef6c2e72bb/transformers-4.57.1-py3-none-any.whl", hash = "sha256:b10d05da8fa67dc41644dbbf9bc45a44cb86ae33da6f9295f5fbf5b7890bd267", size = 11990925, upload-time = "2025-10-14T15:39:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6b/2f416568b3c4c91c96e5a365d164f8a4a4a88030aa8ab4644181fdadce97/transformers-4.57.3-py3-none-any.whl", hash = "sha256:c77d353a4851b1880191603d36acb313411d3577f6e2897814f333841f7003f4", size = 11993463, upload-time = "2025-11-25T15:51:26.493Z" }, ] [[package]] @@ -2589,47 +2584,48 @@ name = "triton" version = "3.5.0" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/87/9b/30988039e1e84df7554fba24e6a734d2d0e847af33cabdf9b532b3c51456/triton-3.5.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da21fccceafc163e3a5e857abe34351ef76345af06cabf9637a914742671f0b", size = 159946647, upload-time = "2025-10-15T19:15:56.325Z" }, { url = "https://files.pythonhosted.org/packages/f5/3a/e991574f3102147b642e49637e0281e9bb7c4ba254edb2bab78247c85e01/triton-3.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9e71db82261c4ffa3921cd050cd5faa18322d2d405c30eb56084afaff3b0833", size = 170476535, upload-time = "2025-10-13T16:38:05.18Z" }, ] [[package]] name = "ty" -version = "0.0.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/db/6299d478000f4f1c6f9bf2af749359381610ffc4cbe6713b66e436ecf6e7/ty-0.0.5.tar.gz", hash = "sha256:983da6330773ff71e2b249810a19c689f9a0372f6e21bbf7cde37839d05b4346", size = 4806218, upload-time = "2025-12-20T21:19:17.24Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/98/c1f61ba378b4191e641bb36c07b7fcc70ff844d61be7a4bf2fea7472b4a9/ty-0.0.5-py3-none-linux_armv6l.whl", hash = "sha256:1594cd9bb68015eb2f5a3c68a040860f3c9306dc6667d7a0e5f4df9967b460e2", size = 9785554, upload-time = "2025-12-20T21:19:05.024Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f9/b37b77c03396bd779c1397dae4279b7ad79315e005b3412feed8812a4256/ty-0.0.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7c0140ba980233d28699d9ddfe8f43d0b3535d6a3bbff9935df625a78332a3cf", size = 9603995, upload-time = "2025-12-20T21:19:15.256Z" }, - { url = "https://files.pythonhosted.org/packages/7d/70/4e75c11903b0e986c0203040472627cb61d6a709e1797fb08cdf9d565743/ty-0.0.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:15de414712cde92048ae4b1a77c4dc22920bd23653fe42acaf73028bad88f6b9", size = 9145815, upload-time = "2025-12-20T21:19:36.481Z" }, - { url = "https://files.pythonhosted.org/packages/89/05/93983dfcf871a41dfe58e5511d28e6aa332a1f826cc67333f77ae41a2f8a/ty-0.0.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:438aa51ad6c5fae64191f8d58876266e26f9250cf09f6624b6af47a22fa88618", size = 9619849, upload-time = "2025-12-20T21:19:19.084Z" }, - { url = "https://files.pythonhosted.org/packages/82/b6/896ab3aad59f846823f202e94be6016fb3f72434d999d2ae9bd0f28b3af9/ty-0.0.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b3d373fd96af1564380caf153600481c676f5002ee76ba8a7c3508cdff82ee0", size = 9606611, upload-time = "2025-12-20T21:19:24.583Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ae/098e33fc92330285ed843e2750127e896140c4ebd2d73df7732ea496f588/ty-0.0.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8453692503212ad316cf8b99efbe85a91e5f63769c43be5345e435a1b16cba5a", size = 10029523, upload-time = "2025-12-20T21:19:07.055Z" }, - { url = "https://files.pythonhosted.org/packages/04/5a/f4b4c33758b9295e9aca0de9645deca0f4addd21d38847228723a6e780fc/ty-0.0.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2e4c454139473abbd529767b0df7a795ed828f780aef8d0d4b144558c0dc4446", size = 10870892, upload-time = "2025-12-20T21:19:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c5/4e3e7e88389365aa1e631c99378711cf0c9d35a67478cb4720584314cf44/ty-0.0.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:426d4f3b82475b1ec75f3cc9ee5a667c8a4ae8441a09fcd8e823a53b706d00c7", size = 10599291, upload-time = "2025-12-20T21:19:26.557Z" }, - { url = "https://files.pythonhosted.org/packages/c1/5d/138f859ea87bd95e17b9818e386ae25a910e46521c41d516bf230ed83ffc/ty-0.0.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5710817b67c6b2e4c0224e4f319b7decdff550886e9020f6d46aa1ce8f89a609", size = 10413515, upload-time = "2025-12-20T21:19:11.094Z" }, - { url = "https://files.pythonhosted.org/packages/27/21/1cbcd0d3b1182172f099e88218137943e0970603492fb10c7c9342369d9a/ty-0.0.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23c55ef08882c7c5ced1ccb90b4eeefa97f690aea254f58ac0987896c590f76", size = 10144992, upload-time = "2025-12-20T21:19:13.225Z" }, - { url = "https://files.pythonhosted.org/packages/ad/30/fdac06a5470c09ad2659a0806497b71f338b395d59e92611f71b623d05a0/ty-0.0.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b9e4c1a28a23b14cf8f4f793f4da396939f16c30bfa7323477c8cc234e352ac4", size = 9606408, upload-time = "2025-12-20T21:19:09.212Z" }, - { url = "https://files.pythonhosted.org/packages/09/93/e99dcd7f53295192d03efd9cbcec089a916f49cad4935c0160ea9adbd53d/ty-0.0.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4e9ebb61529b9745af662e37c37a01ad743cdd2c95f0d1421705672874d806cd", size = 9630040, upload-time = "2025-12-20T21:19:38.165Z" }, - { url = "https://files.pythonhosted.org/packages/d7/f8/6d1e87186e4c35eb64f28000c1df8fd5f73167ce126c5e3dd21fd1204a23/ty-0.0.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5eb191a8e332f50f56dfe45391bdd7d43dd4ef6e60884710fd7ce84c5d8c1eb5", size = 9754016, upload-time = "2025-12-20T21:19:32.79Z" }, - { url = "https://files.pythonhosted.org/packages/28/e6/20f989342cb3115852dda404f1d89a10a3ce93f14f42b23f095a3d1a00c9/ty-0.0.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:92ed7451a1e82ee134a2c24ca43b74dd31e946dff2b08e5c34473e6b051de542", size = 10252877, upload-time = "2025-12-20T21:19:20.787Z" }, - { url = "https://files.pythonhosted.org/packages/57/9d/fc66fa557443233dfad9ae197ff3deb70ae0efcfb71d11b30ef62f5cdcc3/ty-0.0.5-py3-none-win32.whl", hash = "sha256:71f6707e4c1c010c158029a688a498220f28bb22fdb6707e5c20e09f11a5e4f2", size = 9212640, upload-time = "2025-12-20T21:19:30.817Z" }, - { url = "https://files.pythonhosted.org/packages/68/b6/05c35f6dea29122e54af0e9f8dfedd0a100c721affc8cc801ebe2bc2ed13/ty-0.0.5-py3-none-win_amd64.whl", hash = "sha256:2b8b754a0d7191e94acdf0c322747fec34371a4d0669f5b4e89549aef28814ae", size = 10034701, upload-time = "2025-12-20T21:19:28.311Z" }, - { url = "https://files.pythonhosted.org/packages/df/ca/4201ed5cb2af73912663d0c6ded927c28c28b3c921c9348aa8d2cfef4853/ty-0.0.5-py3-none-win_arm64.whl", hash = "sha256:83bea5a5296caac20d52b790ded2b830a7ff91c4ed9f36730fe1f393ceed6654", size = 9566474, upload-time = "2025-12-20T21:19:22.518Z" }, +version = "0.0.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/43/8be3ec2e2ce6119cff9ee3a207fae0cb4f2b4f8ed6534175130a32be24a7/ty-0.0.7.tar.gz", hash = "sha256:90e53b20b86c418ee41a8385f17da44cc7f916f96f9eee87593423ce8292ca72", size = 4826677, upload-time = "2025-12-24T21:28:49.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/56/fafa123acf955089306372add312f16e97aba61f7c4daf74e2bb9c350d23/ty-0.0.7-py3-none-linux_armv6l.whl", hash = "sha256:b30105bd9a0b064497111c50c206d5b6a032f29bcf39f09a12085c3009d72784", size = 9862360, upload-time = "2025-12-24T21:28:36.762Z" }, + { url = "https://files.pythonhosted.org/packages/71/f4/9c30ff498d9a60e24f16d26c0cf93cd03a119913ffa720a77149f02df06e/ty-0.0.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b4df20889115f3d5611a9d9cdedc222e3fd82b5fe87bb0a9f7246e53a23becc7", size = 9712866, upload-time = "2025-12-24T21:28:25.926Z" }, + { url = "https://files.pythonhosted.org/packages/43/84/e06a4a6e4011890027ffee41efbf261b1335103d09009d625ace7f1a60eb/ty-0.0.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f699589d8511e1e17c5a7edfc5f4a4e80f2a6d4a3932a0e9e3422fd32d731472", size = 9221692, upload-time = "2025-12-24T21:28:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e9/ebb4192d3627730125d40ee403a17dc91bab59d69c3eff286453b3218d01/ty-0.0.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3eaec2d8aa153ee4bcc43b17a384d0f9e66177c8c8127be3358b6b8348b9e3b", size = 9710340, upload-time = "2025-12-24T21:28:55.148Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4a/ec144458a9cfb324d5cb471483094e62e74d73179343dff262a5cca1a1e1/ty-0.0.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:177d160295e6a56bdf0b61f6120bc4502fff301d4d10855ba711c109aa7f37fb", size = 9670317, upload-time = "2025-12-24T21:28:43.096Z" }, + { url = "https://files.pythonhosted.org/packages/b6/94/fe7106fd5e2ac06b81fba7b785a6216774618edc3fda9e17f58efe3cede6/ty-0.0.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30518b95ab5cc83615794cca765a5fb86df39a0d9c3dadc0ab2d787ab7830008", size = 10096517, upload-time = "2025-12-24T21:28:23.667Z" }, + { url = "https://files.pythonhosted.org/packages/45/d9/db96ccfd663c96bdd4bb63db72899198c01445012f939477a5318a563f14/ty-0.0.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7867b3f75c2d9602cc6fb3b6d462580b707c2d112d4b27037142b0d01f8bfd03", size = 10996406, upload-time = "2025-12-24T21:28:39.134Z" }, + { url = "https://files.pythonhosted.org/packages/94/da/103915c08c3e6a14f95959614646fcdc9a240cd9a039fadbdcd086c819ee/ty-0.0.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:878d45858e209b7904753fbc5155f4cb75dadc20a26bbb77614bfef31580f9ae", size = 10712829, upload-time = "2025-12-24T21:28:27.745Z" }, + { url = "https://files.pythonhosted.org/packages/47/c0/d9be417bc8e459e13e9698978579eec9868f91f4c5d6ef663249967fec8b/ty-0.0.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:651820b193901825afce40ae68f6a51cd64dbfa4b81a45db90061401261f25e4", size = 10486541, upload-time = "2025-12-24T21:28:45.17Z" }, + { url = "https://files.pythonhosted.org/packages/ad/09/d1858c66620d8ae566e021ad0d7168914b1568841f8fe9e439116ce6b440/ty-0.0.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f56a5a0c1c045863b1b70c358a392b3f73b8528c5c571d409f19dd465525e116", size = 10255312, upload-time = "2025-12-24T21:28:53.17Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0a/78f75089db491fd5fcc13d2845a0b2771b7f7d377450c64c6616e9c227bc/ty-0.0.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:748218fbc1f7b7f1b9d14e77d4f3d7fec72af794417e26b0185bdb94153afe1c", size = 9696201, upload-time = "2025-12-24T21:28:57.345Z" }, + { url = "https://files.pythonhosted.org/packages/01/9e/b26e94832fd563fef6f77a4487affc77a027b0e53106422c66aafb37fa01/ty-0.0.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1ff80f3985a52a7358b9069b4a8d223e92cf312544a934a062d6d3a4fb6876b3", size = 9688907, upload-time = "2025-12-24T21:28:59.485Z" }, + { url = "https://files.pythonhosted.org/packages/5a/8f/cc48601fb92c964cf6c34277e0d947076146b7de47aa11b5dbae45e01ce7/ty-0.0.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a808910ce672ba4446699f4c021283208f58f988bcfc3bdbdfc6e005819d9ee0", size = 9829982, upload-time = "2025-12-24T21:28:34.429Z" }, + { url = "https://files.pythonhosted.org/packages/b5/af/7fa9c2bfa25865968bded637f7e71f1a712f4fbede88f487b6a9101ab936/ty-0.0.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2718fea5f314eda01703fb406ec89b1fc8710b3fc6a09bbd6f7a4f3502ddc889", size = 10361037, upload-time = "2025-12-24T21:28:47.027Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5b/1a6ff1495975cd1c02aa8d03bc5c9d8006eaeb8bf354446f88d70f0518fd/ty-0.0.7-py3-none-win32.whl", hash = "sha256:ae89bb8dc50deb66f34eab3113aa61ac5d7f85ecf16279e5918548085a89021c", size = 9295092, upload-time = "2025-12-24T21:28:51.041Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f6/47e9364635d048002354f84d2d0d6dfc9eb166dc67850739f88e1fec4fc5/ty-0.0.7-py3-none-win_amd64.whl", hash = "sha256:25bd20e3d4d0f07b422f9b42711ba24d28116031273bd23dbda66cec14df1c06", size = 10162816, upload-time = "2025-12-24T21:28:41.006Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f4/c4fc28410c4493982b7481fb23f62bacb02fd2912ebec3b9bc7de18bebb8/ty-0.0.7-py3-none-win_arm64.whl", hash = "sha256:c87d27484dba9fca0053b6a9eee47eecc760aab2bbb8e6eab3d7f81531d1ad0c", size = 9653112, upload-time = "2025-12-24T21:28:31.562Z" }, ] [[package]] name = "typer" -version = "0.20.0" +version = "0.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "typing-extensions" }, + { name = "click", marker = "sys_platform == 'linux'" }, + { name = "rich", marker = "sys_platform == 'linux'" }, + { name = "shellingham", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8f/28/7c85c8032b91dbe79725b6f17d2fffc595dff06a35c7a30a37bef73a1ab4/typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37", size = 106492, upload-time = "2025-10-20T17:03:49.445Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/30/ff9ede605e3bd086b4dd842499814e128500621f7951ca1e5ce84bbf61b1/typer-0.21.0.tar.gz", hash = "sha256:c87c0d2b6eee3b49c5c64649ec92425492c14488096dfbc8a0c2799b2f6f9c53", size = 106781, upload-time = "2025-12-25T09:54:53.651Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028, upload-time = "2025-10-20T17:03:47.617Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e4/5ebc1899d31d2b1601b32d21cfb4bba022ae6fce323d365f0448031b1660/typer-0.21.0-py3-none-any.whl", hash = "sha256:c79c01ca6b30af9fd48284058a7056ba0d3bf5cf10d0ff3d0c5b11b68c258ac6", size = 47109, upload-time = "2025-12-25T09:54:51.918Z" }, ] [[package]] @@ -2664,35 +2660,34 @@ wheels = [ [[package]] name = "urllib3" -version = "2.5.0" +version = "2.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, ] [[package]] name = "uvicorn" -version = "0.38.0" +version = "0.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "h11" }, + { name = "click", marker = "sys_platform == 'linux'" }, + { name = "h11", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, ] [package.optional-dependencies] standard = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "httptools" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, - { name = "watchfiles" }, - { name = "websockets" }, + { name = "httptools", marker = "sys_platform == 'linux'" }, + { name = "python-dotenv", marker = "sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'linux'" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'linux'" }, + { name = "watchfiles", marker = "sys_platform == 'linux'" }, + { name = "websockets", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -2701,8 +2696,6 @@ version = "0.22.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, @@ -2714,65 +2707,65 @@ name = "vllm" version = "0.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiohttp" }, - { name = "anthropic" }, - { name = "blake3" }, - { name = "cachetools" }, - { name = "cbor2" }, - { name = "cloudpickle" }, - { name = "compressed-tensors" }, - { name = "depyf" }, - { name = "diskcache" }, - { name = "einops" }, - { name = "fastapi", extra = ["standard"] }, - { name = "filelock" }, - { name = "flashinfer-python" }, - { name = "gguf" }, - { name = "ijson" }, - { name = "lark" }, - { name = "llguidance", marker = "platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'ppc64le' or platform_machine == 's390x' or platform_machine == 'x86_64'" }, - { name = "lm-format-enforcer" }, - { name = "mcp" }, - { name = "mistral-common", extra = ["image"] }, - { name = "model-hosting-container-standards" }, - { name = "msgspec" }, - { name = "ninja" }, - { name = "numba" }, - { name = "numpy" }, - { name = "openai" }, - { name = "openai-harmony" }, - { name = "opencv-python-headless" }, - { name = "outlines-core" }, - { name = "partial-json-parser" }, - { name = "pillow" }, - { name = "prometheus-client" }, - { name = "prometheus-fastapi-instrumentator" }, - { name = "protobuf" }, - { name = "psutil" }, - { name = "py-cpuinfo" }, - { name = "pybase64" }, - { name = "pydantic" }, - { name = "python-json-logger" }, - { name = "pyyaml" }, - { name = "pyzmq" }, - { name = "ray", extra = ["cgraph"] }, - { name = "regex" }, - { name = "requests" }, - { name = "scipy" }, - { name = "sentencepiece" }, - { name = "setproctitle" }, - { name = "setuptools" }, - { name = "six" }, - { name = "tiktoken" }, - { name = "tokenizers" }, - { name = "torch" }, - { name = "torchaudio" }, - { name = "torchvision" }, - { name = "tqdm" }, - { name = "transformers" }, - { name = "typing-extensions" }, - { name = "watchfiles" }, - { name = "xgrammar", marker = "platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'ppc64le' or platform_machine == 's390x' or platform_machine == 'x86_64'" }, + { name = "aiohttp", marker = "sys_platform == 'linux'" }, + { name = "anthropic", marker = "sys_platform == 'linux'" }, + { name = "blake3", marker = "sys_platform == 'linux'" }, + { name = "cachetools", marker = "sys_platform == 'linux'" }, + { name = "cbor2", marker = "sys_platform == 'linux'" }, + { name = "cloudpickle", marker = "sys_platform == 'linux'" }, + { name = "compressed-tensors", marker = "sys_platform == 'linux'" }, + { name = "depyf", marker = "sys_platform == 'linux'" }, + { name = "diskcache", marker = "sys_platform == 'linux'" }, + { name = "einops", marker = "sys_platform == 'linux'" }, + { name = "fastapi", extra = ["standard"], marker = "sys_platform == 'linux'" }, + { name = "filelock", marker = "sys_platform == 'linux'" }, + { name = "flashinfer-python", marker = "sys_platform == 'linux'" }, + { name = "gguf", marker = "sys_platform == 'linux'" }, + { name = "ijson", marker = "sys_platform == 'linux'" }, + { name = "lark", marker = "sys_platform == 'linux'" }, + { name = "llguidance", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine == 'ppc64le' and sys_platform == 'linux') or (platform_machine == 's390x' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "lm-format-enforcer", marker = "sys_platform == 'linux'" }, + { name = "mcp", marker = "sys_platform == 'linux'" }, + { name = "mistral-common", extra = ["image"], marker = "sys_platform == 'linux'" }, + { name = "model-hosting-container-standards", marker = "sys_platform == 'linux'" }, + { name = "msgspec", marker = "sys_platform == 'linux'" }, + { name = "ninja", marker = "sys_platform == 'linux'" }, + { name = "numba", marker = "sys_platform == 'linux'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, + { name = "openai", marker = "sys_platform == 'linux'" }, + { name = "openai-harmony", marker = "sys_platform == 'linux'" }, + { name = "opencv-python-headless", marker = "sys_platform == 'linux'" }, + { name = "outlines-core", marker = "sys_platform == 'linux'" }, + { name = "partial-json-parser", marker = "sys_platform == 'linux'" }, + { name = "pillow", marker = "sys_platform == 'linux'" }, + { name = "prometheus-client", marker = "sys_platform == 'linux'" }, + { name = "prometheus-fastapi-instrumentator", marker = "sys_platform == 'linux'" }, + { name = "protobuf", marker = "sys_platform == 'linux'" }, + { name = "psutil", marker = "sys_platform == 'linux'" }, + { name = "py-cpuinfo", marker = "sys_platform == 'linux'" }, + { name = "pybase64", marker = "sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'linux'" }, + { name = "python-json-logger", marker = "sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'linux'" }, + { name = "pyzmq", marker = "sys_platform == 'linux'" }, + { name = "ray", extra = ["cgraph"], marker = "sys_platform == 'linux'" }, + { name = "regex", marker = "sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'linux'" }, + { name = "scipy", marker = "sys_platform == 'linux'" }, + { name = "sentencepiece", marker = "sys_platform == 'linux'" }, + { name = "setproctitle", marker = "sys_platform == 'linux'" }, + { name = "setuptools", marker = "sys_platform == 'linux'" }, + { name = "six", marker = "sys_platform == 'linux'" }, + { name = "tiktoken", marker = "sys_platform == 'linux'" }, + { name = "tokenizers", marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.9.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, + { name = "torchaudio", marker = "sys_platform == 'linux'" }, + { name = "torchvision", marker = "sys_platform == 'linux'" }, + { name = "tqdm", marker = "sys_platform == 'linux'" }, + { name = "transformers", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, + { name = "watchfiles", marker = "sys_platform == 'linux'" }, + { name = "xgrammar", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine == 'ppc64le' and sys_platform == 'linux') or (platform_machine == 's390x' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/11/12/b922f96778d07df1c28dfa9a81fbc9706c13c5d0a4e8d154060818a79705/vllm-0.13.0.tar.gz", hash = "sha256:4ad43db45fef37114b550d03a4f423fb3fa3a31d8bc09ee810ef8b9cdcd4b5fe", size = 17828199, upload-time = "2025-12-19T03:30:32.741Z" } wheels = [ @@ -2789,17 +2782,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/96/04e7b441807b26b794da5b11e59ed7f83b2cf8af202bd7eba8ad2fa6046e/wadler_lindig-0.1.7-py3-none-any.whl", hash = "sha256:e3ec83835570fd0a9509f969162aeb9c65618f998b1f42918cfc8d45122fe953", size = 20516, upload-time = "2025-06-18T07:00:41.684Z" }, ] +[[package]] +name = "wandb" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "gitpython" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sentry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/cc/770ae3aa7ae44f6792f7ecb81c14c0e38b672deb35235719bb1006519487/wandb-0.23.1.tar.gz", hash = "sha256:f6fb1e3717949b29675a69359de0eeb01e67d3360d581947d5b3f98c273567d6", size = 44298053, upload-time = "2025-12-03T02:25:10.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/0b/c3d7053dfd93fd259a63c7818d9c4ac2ba0642ff8dc8db98662ea0cf9cc0/wandb-0.23.1-py3-none-macosx_12_0_arm64.whl", hash = "sha256:358e15471d19b7d73fc464e37371c19d44d39e433252ac24df107aff993a286b", size = 21527293, upload-time = "2025-12-03T02:24:48.011Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9f/059420fa0cb6c511dc5c5a50184122b6aca7b178cb2aa210139e354020da/wandb-0.23.1-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:110304407f4b38f163bdd50ed5c5225365e4df3092f13089c30171a75257b575", size = 22745926, upload-time = "2025-12-03T02:24:50.519Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fd465827c14c64d056d30b4c9fcf4dac889a6969dba64489a88fc4ffa333/wandb-0.23.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:6cc984cf85feb2f8ee0451d76bc9fb7f39da94956bb8183e30d26284cf203b65", size = 21212973, upload-time = "2025-12-03T02:24:52.828Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ee/9a8bb9a39cc1f09c3060456cc79565110226dc4099a719af5c63432da21d/wandb-0.23.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:67431cd3168d79fdb803e503bd669c577872ffd5dadfa86de733b3274b93088e", size = 22887885, upload-time = "2025-12-03T02:24:55.281Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4d/8d9e75add529142e037b05819cb3ab1005679272950128d69d218b7e5b2e/wandb-0.23.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:07be70c0baa97ea25fadc4a9d0097f7371eef6dcacc5ceb525c82491a31e9244", size = 21250967, upload-time = "2025-12-03T02:24:57.603Z" }, + { url = "https://files.pythonhosted.org/packages/97/72/0b35cddc4e4168f03c759b96d9f671ad18aec8bdfdd84adfea7ecb3f5701/wandb-0.23.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:216c95b08e0a2ec6a6008373b056d597573d565e30b43a7a93c35a171485ee26", size = 22988382, upload-time = "2025-12-03T02:25:00.518Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6d/e78093d49d68afb26f5261a70fc7877c34c114af5c2ee0ab3b1af85f5e76/wandb-0.23.1-py3-none-win32.whl", hash = "sha256:fb5cf0f85692f758a5c36ab65fea96a1284126de64e836610f92ddbb26df5ded", size = 22150756, upload-time = "2025-12-03T02:25:02.734Z" }, + { url = "https://files.pythonhosted.org/packages/05/27/4f13454b44c9eceaac3d6e4e4efa2230b6712d613ff9bf7df010eef4fd18/wandb-0.23.1-py3-none-win_amd64.whl", hash = "sha256:21c8c56e436eb707b7d54f705652e030d48e5cfcba24cf953823eb652e30e714", size = 22150760, upload-time = "2025-12-03T02:25:05.106Z" }, + { url = "https://files.pythonhosted.org/packages/30/20/6c091d451e2a07689bfbfaeb7592d488011420e721de170884fedd68c644/wandb-0.23.1-py3-none-win_arm64.whl", hash = "sha256:8aee7f3bb573f2c0acf860f497ca9c684f9b35f2ca51011ba65af3d4592b77c1", size = 20137463, upload-time = "2025-12-03T02:25:08.317Z" }, +] + [[package]] name = "watchfiles" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, + { name = "anyio", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, - { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, @@ -2808,9 +2828,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, - { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, - { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, ] [[package]] @@ -2819,50 +2836,32 @@ 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/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] -[[package]] -name = "win32-setctime" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, -] - [[package]] name = "xgrammar" version = "0.1.27" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mlx-lm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "ninja" }, - { name = "numpy" }, - { name = "pydantic" }, - { name = "torch" }, - { name = "transformers" }, + { name = "ninja", marker = "sys_platform == 'linux'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.9.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, + { name = "transformers", marker = "sys_platform == 'linux'" }, { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] 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" }, ] [[package]]