From ab9dc79a32f0d312e08754c3918faa0bcf0cecf9 Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 7 Apr 2026 15:50:18 +0000 Subject: [PATCH 01/54] =?UTF-8?q?feat:=20add=20renderers=20support=20for?= =?UTF-8?q?=20RL=20=E2=80=94=20rendering=20proxy=20+=20/v1/generate=20endp?= =?UTF-8?q?oint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrates the renderers package (from verifiers) into prime-rl's RL pipeline. The Renderer owns all text tokenization; vLLM handles image processing + generation. New files: - serving_generate.py: /v1/generate endpoint — accepts token IDs + optional raw images. Text tokenization done client-side, image processing done server-side. No Jinja chat template application. Works for both text-only and VLM. - utils/messages.py: message normalization utilities Changes: - orchestrator.py: creates Renderer + RenderingProxy, routes rollout traffic through proxy which renders messages to tokens before forwarding to vLLM. Uses pretokenize_rollout_trajectory with renderer for training tokenization. - server.py: registers /v1/generate route and handler - trajectories.py: pretokenize_rollout_trajectory accepts optional renderer - configs/shared.py: model.renderer config field ('auto' or explicit) Validated E2E on vLLM 0.19: - Text RL: reverse-text, 5 steps - VLM RL: color-codeword with Qwen3-VL-4B, 3 turns x 2 images, 10 steps Co-Authored-By: Claude Opus 4.6 (1M context) --- pyproject.toml | 2 +- src/prime_rl/configs/shared.py | 10 + src/prime_rl/inference/vllm/server.py | 26 +++ .../inference/vllm/serving_generate.py | 169 ++++++++++++++ src/prime_rl/orchestrator/orchestrator.py | 47 +++- src/prime_rl/orchestrator/trajectories.py | 46 +++- src/prime_rl/utils/messages.py | 62 +++++ uv.lock | 221 ++++++++---------- 8 files changed, 429 insertions(+), 154 deletions(-) create mode 100644 src/prime_rl/inference/vllm/serving_generate.py create mode 100644 src/prime_rl/utils/messages.py diff --git a/pyproject.toml b/pyproject.toml index a123e38b771..cd8be303c5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,7 +120,7 @@ override-dependencies = [ [tool.uv.sources] torch = { index = "pytorch-cu128" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "0760204" } +verifiers = { path = "/home/ubuntu/verifiers", editable = true } torchtitan = { git = "https://github.com/pytorch/torchtitan", rev = "a1fdd7e" } dion = { git = "https://github.com/samsja/dion.git", rev = "d891eeb" } transformers = { git = "https://github.com/huggingface/transformers.git", rev = "c1c3424" } diff --git a/src/prime_rl/configs/shared.py b/src/prime_rl/configs/shared.py index 0438ac86fbc..2b32d4a2649 100644 --- a/src/prime_rl/configs/shared.py +++ b/src/prime_rl/configs/shared.py @@ -123,6 +123,16 @@ class BaseModelConfig(BaseConfig): ), ] = False + renderer: Annotated[ + str, + Field( + description=( + "Renderer to use for chat template tokenization. " + "Options: 'auto' (detect from tokenizer), 'qwen3', 'qwen3.5', 'glm5', 'glm4.5', 'minimax-m2', 'default'." + ), + ), + ] = "auto" + vlm: Annotated[ "VLMConfig | None", Field( diff --git a/src/prime_rl/inference/vllm/server.py b/src/prime_rl/inference/vllm/server.py index 71ff2c3577f..dd1e845a49e 100644 --- a/src/prime_rl/inference/vllm/server.py +++ b/src/prime_rl/inference/vllm/server.py @@ -179,6 +179,27 @@ def chat_with_tokens(request: Request) -> OpenAIServingChatWithTokens | None: return request.app.state.openai_serving_chat_with_tokens +def generate_handler(request: Request): + return request.app.state.openai_serving_generate + + +@router.post("/v1/generate") +@with_cancellation +@load_aware_call +async def _generate(raw_request: Request): + from prime_rl.inference.vllm.serving_generate import GenerateRequest + + body = await raw_request.json() + request = GenerateRequest(**body) + handler = generate_handler(raw_request) + if handler is None: + return JSONResponse({"error": "generate endpoint not available"}, status_code=503) + result = await handler.generate(request, raw_request) + if isinstance(result, dict) and "error" in result: + return JSONResponse(result, status_code=500) + return JSONResponse(content=result.model_dump()) + + @router.post("/pause") async def pause(request: Request): await engine_client(request).pause_generation(mode="keep", clear_cache=False) @@ -283,6 +304,11 @@ async def custom_init_app_state( else: state.openai_serving_chat_with_tokens = None + # /v1/generate endpoint — tokens + optional images, no chat template + from prime_rl.inference.vllm.serving_generate import OpenAIServingGenerate + + state.openai_serving_generate = OpenAIServingGenerate(engine_client) + import vllm.entrypoints.openai.api_server from vllm.entrypoints.openai.api_server import build_app as _original_build_app diff --git a/src/prime_rl/inference/vllm/serving_generate.py b/src/prime_rl/inference/vllm/serving_generate.py new file mode 100644 index 00000000000..b5cec8f156b --- /dev/null +++ b/src/prime_rl/inference/vllm/serving_generate.py @@ -0,0 +1,169 @@ +"""/v1/generate endpoint — accepts pre-tokenized + pre-processed inputs. + +Text-only: accepts prompt_token_ids, passes to engine. +VLM: accepts prompt_token_ids + raw images (multi_modal_data), vLLM processes +images and expands placeholders. The Renderer does all text tokenization +client-side — vLLM just handles image processing and generation. + +No Jinja rendering, no server-side chat template application. +""" + +from __future__ import annotations + +import base64 +from io import BytesIO +from typing import Any +from uuid import uuid4 + +import numpy as np +from fastapi import Request +from PIL import Image +from pydantic import BaseModel, Field +from vllm.engine.protocol import EngineClient +from vllm.logger import init_logger +from vllm.outputs import RequestOutput +from vllm.sampling_params import SamplingParams + +logger = init_logger(__name__) + + +# ── Request / Response schemas ─────────────────────────────────────── + + +class RawImageData(BaseModel): + """Raw image bytes — vLLM processes them server-side.""" + + data: str = Field(description="Base64-encoded image bytes") + media_type: str = Field(default="image/png") + + +class GenerateRequest(BaseModel): + model: str | None = None + prompt_token_ids: list[int] + images: list[RawImageData] | None = None + + max_tokens: int = 4096 + temperature: float = 1.0 + top_p: float = 1.0 + top_k: int = -1 + min_p: float = 0.0 + seed: int | None = None + n: int = 1 + stop_token_ids: list[int] | None = None + repetition_penalty: float = 1.0 + min_tokens: int = 0 + + +class GenerateChoiceResponse(BaseModel): + index: int + token_ids: list[int] + logprobs: list[float] + finish_reason: str | None = None + routed_experts: dict | None = None + + +class GenerateResponse(BaseModel): + id: str + model: str + prompt_token_ids: list[int] + choices: list[GenerateChoiceResponse] + usage: dict + + +# ── Handler ────────────────────────────────────────────────────────── + + +class OpenAIServingGenerate: + """Lightweight generate handler — tokens + optional images in, tokens out.""" + + def __init__(self, engine_client: EngineClient): + self.engine_client = engine_client + + async def generate(self, request: GenerateRequest, raw_request: Request) -> GenerateResponse | dict: + engine_prompt: dict[str, Any] = { + "prompt_token_ids": request.prompt_token_ids, + } + + if request.images: + pil_images = [Image.open(BytesIO(base64.b64decode(img.data))) for img in request.images] + engine_prompt["multi_modal_data"] = {"image": pil_images} + + sampling_params = SamplingParams( + max_tokens=request.max_tokens, + temperature=request.temperature, + top_p=request.top_p, + top_k=request.top_k, + min_p=request.min_p, + seed=request.seed, + n=request.n, + stop_token_ids=request.stop_token_ids or [], + repetition_penalty=request.repetition_penalty, + min_tokens=request.min_tokens, + logprobs=1, + skip_special_tokens=False, + ) + + request_id = f"gen-{uuid4().hex[:16]}" + routed_experts_map: dict[int, dict] = {} + final_output: RequestOutput | None = None + + generator = self.engine_client.generate( + engine_prompt, + sampling_params, + request_id, + ) + + async for output in generator: + if await raw_request.is_disconnected(): + await self.engine_client.abort(request_id) + return {"error": "Client disconnected"} + for comp_output in output.outputs: + if comp_output.routed_experts is not None: + routed_experts_map[comp_output.index] = _encode_routed_experts(comp_output.routed_experts) + final_output = output + + if final_output is None: + return {"error": "No output generated"} + + choices = [] + for output in final_output.outputs: + token_ids = list(output.token_ids) + logprobs_list: list[float] = [] + if output.logprobs: + for i, lp_dict in enumerate(output.logprobs): + if i < len(token_ids) and token_ids[i] in lp_dict: + logprobs_list.append(lp_dict[token_ids[i]].logprob) + else: + logprobs_list.append(0.0) + + choices.append( + GenerateChoiceResponse( + index=output.index, + token_ids=token_ids, + logprobs=logprobs_list, + finish_reason=output.finish_reason, + routed_experts=routed_experts_map.get(output.index), + ) + ) + + prompt_len = len(final_output.prompt_token_ids) + completion_len = sum(len(c.token_ids) for c in choices) + + return GenerateResponse( + id=request_id, + model=request.model or "", + prompt_token_ids=list(final_output.prompt_token_ids), + choices=choices, + usage={ + "prompt_tokens": prompt_len, + "completion_tokens": completion_len, + "total_tokens": prompt_len + completion_len, + }, + ) + + +def _encode_routed_experts(arr: np.ndarray) -> dict: + return { + "data": base64.b85encode(arr.tobytes()).decode("ascii"), + "shape": list(arr.shape), + } diff --git a/src/prime_rl/orchestrator/orchestrator.py b/src/prime_rl/orchestrator/orchestrator.py index 5227d0358ac..253fe7cf430 100644 --- a/src/prime_rl/orchestrator/orchestrator.py +++ b/src/prime_rl/orchestrator/orchestrator.py @@ -59,7 +59,10 @@ wait_for_env_servers, ) from prime_rl.utils.client import ( + StaticInferencePool, init_nccl_broadcast, + setup_admin_clients, + setup_clients, setup_inference_pool, ) from prime_rl.utils.config import cli @@ -113,16 +116,6 @@ async def orchestrate(config: OrchestratorConfig): # Setup rollout inference pool (handles both static and elastic modes) rollout_client_config, rollout_model_name, enable_policy_updates = setup_external_rollout_model(config, logger) - client_type = "openai_chat_completions_token" if config.use_token_client else "openai_chat_completions" - if config.use_token_client: - logger.warning( - "Token-in-token-out (TITO) client is enabled. Only use this if your environment has a linear " - "history and the chat template has the extension property." - ) - inference_pool = await setup_inference_pool( - rollout_client_config, model_name=rollout_model_name, client_type=client_type - ) - # Setup teacher inference pool if configured if config.teacher_model: logger.info( @@ -149,6 +142,36 @@ async def orchestrate(config: OrchestratorConfig): config.model.name, trust_remote_code=config.model.trust_remote_code, use_fast=True ) + # Create renderer and start rendering proxy. + # The proxy sits between verifiers and vLLM: verifiers sends standard chat + # messages → proxy renders to tokens via Renderer → forwards to vLLM /v1/generate. + from renderers.base import create_renderer as _create_renderer + + renderer = _create_renderer(tokenizer, renderer=config.model.renderer) + logger.info(f"Initialized {type(renderer).__name__} for {config.model.name}") + + import uvicorn + from renderers.proxy import RenderingProxy + + proxy = RenderingProxy(renderer, vllm_base_url=rollout_client_config.base_url[0], processor=processor) + proxy_port = 18100 + proxy_server = uvicorn.Server(uvicorn.Config(proxy.app, host="127.0.0.1", port=proxy_port, log_level="warning")) + asyncio.create_task(proxy_server.serve()) + logger.info(f"Started rendering proxy on port {proxy_port} → {rollout_client_config.base_url[0]}") + + from copy import deepcopy + + proxy_client_config = deepcopy(rollout_client_config) + proxy_client_config.base_url = [f"http://127.0.0.1:{proxy_port}/v1"] + + # Rollout clients go through proxy (standard chat format), admin clients talk to vLLM directly + client_type = "openai_chat_completions" + inference_pool = StaticInferencePool( + clients=setup_clients(proxy_client_config, client_type=client_type), + admin_clients=setup_admin_clients(rollout_client_config), + skip_model_check=rollout_client_config.skip_model_check, + ) + # Setup monitor logger.info(f"Initializing monitor (wandb={config.wandb}, prime_monitor={config.prime_monitor})") monitor = setup_monitor( @@ -567,9 +590,9 @@ def _cleanup_env_processes(): # Convert rollouts to training samples parallel_preprocess_start = time.perf_counter() - # Pretokenize before VLM image cache build (which strips image data from messages) + # Pretokenize using the Renderer (same chat template used during rollout) for rollout in train_rollouts: - pretokenize_rollout_trajectory(rollout, tokenizer, processor=processor) + pretokenize_rollout_trajectory(rollout, tokenizer, processor=processor, renderer=renderer) # VLM: build image cache in a thread so it doesn't block the event loop. # This lets the scheduler continue servicing inflight rollout requests diff --git a/src/prime_rl/orchestrator/trajectories.py b/src/prime_rl/orchestrator/trajectories.py index bb335647db3..bbac35a59d2 100644 --- a/src/prime_rl/orchestrator/trajectories.py +++ b/src/prime_rl/orchestrator/trajectories.py @@ -199,12 +199,32 @@ def _get(tool: Any, key: str) -> Any: ] +def _tokenize_step_with_renderer( + step: vf.TrajectoryStep, + renderer, + tools: list[dict[str, Any]] | None = None, +) -> dict[str, Any]: + """Tokenize a trajectory step using a Renderer.""" + from renderers.base import build_trajectory_step + + prompt = _normalize_messages(step.get("prompt"), default_role="user") + completion = _normalize_messages(step.get("completion"), default_role="assistant") + prompt = _strip_message_content(_deserialize_tool_calls(prompt)) + completion = _strip_message_content(_deserialize_tool_calls(completion)) + return build_trajectory_step(renderer, prompt, completion, tools=tools) + + def pretokenize_rollout_trajectory( output: vf.RolloutOutput, tokenizer: PreTrainedTokenizer, processor=None, + renderer=None, ) -> bool: - """Populate missing step tokens from prompt/completion messages.""" + """Populate missing step tokens from prompt/completion messages. + + When a renderer is provided, uses it for tokenization (faster, deterministic). + Otherwise falls back to the tokenizer + apply_chat_template path. + """ logger = get_logger() tools = _convert_tools_to_oai_format(output.get("tool_defs", [])) @@ -212,17 +232,19 @@ def pretokenize_rollout_trajectory( if step["tokens"] is not None: continue - reconstructed = _tokenize_step_from_messages(step, tokenizer, tools=tools, processor=processor) - if reconstructed["prompt_prefix_len"] < reconstructed["original_prompt_len"]: - logger.debug( - f"Prompt tokenization was non-prefix for example {output['example_id']} step {step_idx}. " - f"Using longest common prefix length {reconstructed['prompt_prefix_len']} " - f"(original prompt had {reconstructed['original_prompt_len']} tokens)." - ) - - reconstructed.pop("prompt_prefix_len") - reconstructed.pop("original_prompt_len") - step["tokens"] = reconstructed + if renderer is not None: + step["tokens"] = _tokenize_step_with_renderer(step, renderer, tools=tools) + else: + reconstructed = _tokenize_step_from_messages(step, tokenizer, tools=tools, processor=processor) + if reconstructed["prompt_prefix_len"] < reconstructed["original_prompt_len"]: + logger.debug( + f"Prompt tokenization was non-prefix for example {output['example_id']} step {step_idx}. " + f"Using longest common prefix length {reconstructed['prompt_prefix_len']} " + f"(original prompt had {reconstructed['original_prompt_len']} tokens)." + ) + reconstructed.pop("prompt_prefix_len") + reconstructed.pop("original_prompt_len") + step["tokens"] = reconstructed return True diff --git a/src/prime_rl/utils/messages.py b/src/prime_rl/utils/messages.py new file mode 100644 index 00000000000..13ebf6a6dcf --- /dev/null +++ b/src/prime_rl/utils/messages.py @@ -0,0 +1,62 @@ +"""Message utilities — normalize, deserialize tool calls, strip content.""" + +import json +from typing import Any + + +def normalize_messages(messages: Any, default_role: str) -> list[dict[str, Any]]: + if messages is None: + return [] + if isinstance(messages, str): + return [{"role": default_role, "content": messages}] + if isinstance(messages, dict): + return [dict(messages)] + if isinstance(messages, list): + normalized: list[dict[str, Any]] = [] + for message in messages: + if isinstance(message, str): + normalized.append({"role": default_role, "content": message}) + elif isinstance(message, dict): + normalized.append(dict(message)) + else: + raise TypeError(f"Unsupported message type: {type(message)}") + return normalized + raise TypeError(f"Unsupported messages container type: {type(messages)}") + + +def deserialize_tool_calls(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: + def _deserialize_tool_call(tool_call: dict[str, Any]) -> dict[str, Any]: + function = tool_call.get("function", {}) + arguments = function.get("arguments") + if isinstance(arguments, str): + arguments = json.loads(arguments) + return { + **tool_call, + "function": {**function, "arguments": arguments}, + } + + deserialized_messages: list[dict[str, Any]] = [] + for message in messages: + if "tool_calls" not in message: + deserialized_messages.append(dict(message)) + continue + + tool_calls = message.get("tool_calls") or [] + deserialized_messages.append( + { + **message, + "tool_calls": [_deserialize_tool_call(tc) for tc in tool_calls], + } + ) + + return deserialized_messages + + +def strip_message_content(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: + def _strip(message: dict[str, Any]) -> dict[str, Any]: + content = message.get("content") + if isinstance(content, str): + return {**message, "content": content.strip()} + return message + + return [_strip(message) for message in messages] diff --git a/uv.lock b/uv.lock index 486f16c1ea1..015549bc9a2 100644 --- a/uv.lock +++ b/uv.lock @@ -84,16 +84,8 @@ dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, - { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, - { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, - { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, - { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, ] @@ -238,7 +230,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, - { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, @@ -249,7 +240,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, @@ -288,9 +278,6 @@ source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/08/22b6326dbe002ca77c92082b37b14a935003897b0e3eed025da92c700751/blake3-1.0.5.tar.gz", hash = "sha256:7bac73f393a67ea6d5ac32e4a45d39c184487c89c712ab3ed839c1a51ed82259", size = 115140, upload-time = "2025-05-19T20:08:29.911Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c1/40/b81a25077df6fa1722be8c268732205281e12a244f9d5a15e9e72c2baa04/blake3-1.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abe84cc2db3172bbade48dbf7b6029decb82e9cd382bc3cb783b8624a3ee55d8", size = 374599, upload-time = "2025-05-19T20:07:06.951Z" }, - { url = "https://files.pythonhosted.org/packages/58/1b/8fc14c7b7ae116edc42f8e8cd5c21a99d8b68ab761e31347c4c9c6bbedf6/blake3-1.0.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca8935b4a733968a463d6445dc7cb0dcc09759c280df4847f020deec8fcaff27", size = 375221, upload-time = "2025-05-19T20:07:08.39Z" }, - { url = "https://files.pythonhosted.org/packages/ce/91/e335f22765d7e80fd5aa6a25b2f2f5f0c5d649049f88d0c8ac1f6a8c431d/blake3-1.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15ecd628f824d5591a1958babd4217749f1facd3945f33a14c3e5fbb52ffb922", size = 509907, upload-time = "2025-05-19T20:07:11.023Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ec/c1676c275592efdb3a6e4489d0f5e029d38565593466ba70c42b73e76b1a/blake3-1.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a12b12df3c40089bf2785c333f8f1161b2a66ecacb44828de9fbf2868037934b", size = 395611, upload-time = "2025-05-19T20:07:12.815Z" }, { url = "https://files.pythonhosted.org/packages/5c/04/a86bfb3c20e859e43ead0b13be59afd98feb166ea929e76fa3d190f65f6e/blake3-1.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e8d36e33f413938230683f192f0565f44ee2b050ad92fb94b343706f3df55", size = 384757, upload-time = "2025-05-19T20:07:14.122Z" }, { url = "https://files.pythonhosted.org/packages/6b/bf/93ce719f88b48d5bcdf2f765789a5a955ea6a02a33f310321508c8421ad6/blake3-1.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7083e1b2cfb737c812e20d790c232c38045c7bfe37ef02526f395d491f90f213", size = 551032, upload-time = "2025-05-19T20:07:15.56Z" }, { url = "https://files.pythonhosted.org/packages/13/99/a2e644e0a2039977beb67abbc1f48f6f6c7e0f0c345665811cfa2880b196/blake3-1.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:21240932fc914fd719e2d33297f29742c28a31d8a96cb666ec4679bf2c35aa48", size = 555543, upload-time = "2025-05-19T20:07:17.056Z" }, @@ -350,8 +337,6 @@ dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, @@ -373,7 +358,6 @@ source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/29/59/133001a6a7549dd34a3c28d1358122b0e68a59f27840efb2102b72eb73cf/chardet-7.4.0.post1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45adca296eddec38b803ce352bffcc5fff40576246e74fcd2aa358f9c813ffe0", size = 859436, upload-time = "2026-03-26T19:17:30.2Z" }, { url = "https://files.pythonhosted.org/packages/c3/00/2eec7b47263524f204b3225d023f7d75e9c06a0a75c06a4c85faf2aec246/chardet-7.4.0.post1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f03aef24d91168232db8b01e4d6726676708be503e6aa07e28ab808e6d0fe606", size = 868655, upload-time = "2026-03-26T19:17:31.871Z" }, - { url = "https://files.pythonhosted.org/packages/00/a2/36e5b1a46a36293cac237fa5c61f9e11497e025ec2e4b10e8d187dede9b9/chardet-7.4.0.post1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5aec4e167de05470a3e2466a1ae751d7f0ad15510f63633fdd01a0405515df7a", size = 862406, upload-time = "2026-03-26T19:17:33.529Z" }, { url = "https://files.pythonhosted.org/packages/91/d7/47988d40231b41376f5a66346ef3b322c81091dfd4c0f84df5a1e3bb06b5/chardet-7.4.0.post1-py3-none-any.whl", hash = "sha256:57a62ef50f69bc2fb3a3ea1ffffec6d10f3d2112d3b05d6e3cb15c2c9b55f6cc", size = 624666, upload-time = "2026-03-26T19:17:51.248Z" }, ] @@ -384,12 +368,8 @@ source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, - { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, - { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, - { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] @@ -533,22 +513,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, @@ -980,14 +954,8 @@ source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload-time = "2025-06-09T23:00:46.125Z" }, - { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload-time = "2025-06-09T23:00:47.73Z" }, - { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload-time = "2025-06-09T23:00:49.742Z" }, - { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload-time = "2025-06-09T23:00:51.352Z" }, { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload-time = "2025-06-09T23:00:54.43Z" }, { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload-time = "2025-06-09T23:00:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload-time = "2025-06-09T23:00:58.468Z" }, - { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload-time = "2025-06-09T23:01:01.474Z" }, - { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload-time = "2025-06-09T23:01:02.961Z" }, { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload-time = "2025-06-09T23:01:05.095Z" }, { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, ] @@ -1098,7 +1066,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/0b/bb/d89b2f8ed062af360e872746cab9d5a98acf80f9fec537536203695cce63/grpcio-1.78.0rc2.tar.gz", hash = "sha256:d624592c82a19a5898c5576fbda43c28d7062bac04ea6f33bbd8871bc0639e64", size = 12831859, upload-time = "2026-01-16T07:30:23.727Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/08/ca0f91793817a002a775b0a3918d88645237bf3d69c7e53dcc7c5769a1bc/grpcio-1.78.0rc2-cp312-cp312-linux_armv7l.whl", hash = "sha256:5ccf4496425b5f5a7a9b801d79fe5e8bfbdf2408b2ab976f291f3e1536d4a3f7", size = 5914063, upload-time = "2026-01-16T07:28:33.049Z" }, { url = "https://files.pythonhosted.org/packages/2e/e7/94e4c7ae7fdbaf7adc8af47eb6e3b53166c184b281a470210df8bf0dcb96/grpcio-1.78.0rc2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7fe343a2ccaa3ca48a933e81f4c0a9de37057cf5bc5567864a98775cce570456", size = 6476173, upload-time = "2026-01-16T07:28:38.599Z" }, { url = "https://files.pythonhosted.org/packages/c4/d7/60e821443c044365222a7fcd6630344016ea3e31bf4903f3a22f93e5b3a1/grpcio-1.78.0rc2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7bde54ad7bee2d4dbc6d4a351d5b62cc2bfa87c58e9db911ed8a0489192ca9a", size = 6690812, upload-time = "2026-01-16T07:28:43.887Z" }, { url = "https://files.pythonhosted.org/packages/80/47/b19c67ca6e0622fccb88558452e6f7458551ef365456585968dcd84a1db3/grpcio-1.78.0rc2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e99bbce4f509eb6af4b523109152258043b92bbb945950456a9318eca71ef2e", size = 7266122, upload-time = "2026-01-16T07:28:46.109Z" }, @@ -1422,9 +1389,6 @@ source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/85/1ce02cade7516b726dd88f59a4ee46914bf79d1676d1228ef2002ed2f1c9/jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2", size = 345330, upload-time = "2025-05-18T19:03:47.596Z" }, - { url = "https://files.pythonhosted.org/packages/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670, upload-time = "2025-05-18T19:03:49.334Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057, upload-time = "2025-05-18T19:03:50.66Z" }, - { url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372, upload-time = "2025-05-18T19:03:51.98Z" }, { url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038, upload-time = "2025-05-18T19:03:53.703Z" }, { url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557, upload-time = "2025-05-18T19:03:56.386Z" }, { url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202, upload-time = "2025-05-18T19:03:57.675Z" }, @@ -1851,11 +1815,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/91/1a/edb23803a168f070d wheels = [ { url = "https://files.pythonhosted.org/packages/e5/e2/51ed62063b44d10b06d975ac87af287729eeb5e3ed9772f7584a17983e90/mmh3-5.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006", size = 103274, upload-time = "2026-03-05T15:54:26.44Z" }, { url = "https://files.pythonhosted.org/packages/75/ce/12a7524dca59eec92e5b31fdb13ede1e98eda277cf2b786cf73bfbc24e81/mmh3-5.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825", size = 106158, upload-time = "2026-03-05T15:54:28.578Z" }, - { url = "https://files.pythonhosted.org/packages/86/1f/d3ba6dd322d01ab5d44c46c8f0c38ab6bbbf9b5e20e666dfc05bf4a23604/mmh3-5.2.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a", size = 113005, upload-time = "2026-03-05T15:54:29.767Z" }, - { url = "https://files.pythonhosted.org/packages/b6/a9/15d6b6f913294ea41b44d901741298e3718e1cb89ee626b3694625826a43/mmh3-5.2.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b", size = 120744, upload-time = "2026-03-05T15:54:30.931Z" }, { url = "https://files.pythonhosted.org/packages/76/b3/70b73923fd0284c439860ff5c871b20210dfdbe9a6b9dd0ee6496d77f174/mmh3-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166", size = 99111, upload-time = "2026-03-05T15:54:32.353Z" }, - { url = "https://files.pythonhosted.org/packages/fd/68/6e292c0853e204c44d2f03ea5f090be3317a0e2d9417ecb62c9eb27687df/mmh3-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211", size = 106437, upload-time = "2026-03-05T15:54:35.177Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c6/fedd7284c459cfb58721d461fcf5607a4c1f5d9ab195d113d51d10164d16/mmh3-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000", size = 110002, upload-time = "2026-03-05T15:54:36.673Z" }, { url = "https://files.pythonhosted.org/packages/3b/ac/ca8e0c19a34f5b71390171d2ff0b9f7f187550d66801a731bb68925126a4/mmh3-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5", size = 97507, upload-time = "2026-03-05T15:54:37.804Z" }, ] @@ -1942,14 +1902,8 @@ source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/45/8b3d6dbad8cf3252553cc41abea09ad527b33ce47a5e199072620b296902/multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", size = 258299, upload-time = "2025-08-11T12:06:58.946Z" }, - { url = "https://files.pythonhosted.org/packages/3c/e8/8ca2e9a9f5a435fc6db40438a55730a4bf4956b554e487fa1b9ae920f825/multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", size = 242357, upload-time = "2025-08-11T12:07:00.301Z" }, - { url = "https://files.pythonhosted.org/packages/0f/84/80c77c99df05a75c28490b2af8f7cba2a12621186e0a8b0865d8e745c104/multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", size = 268369, upload-time = "2025-08-11T12:07:01.638Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e9/920bfa46c27b05fb3e1ad85121fd49f441492dca2449c5bcfe42e4565d8a/multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", size = 269341, upload-time = "2025-08-11T12:07:02.943Z" }, { url = "https://files.pythonhosted.org/packages/af/65/753a2d8b05daf496f4a9c367fe844e90a1b2cac78e2be2c844200d10cc4c/multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", size = 256100, upload-time = "2025-08-11T12:07:04.564Z" }, { url = "https://files.pythonhosted.org/packages/09/54/655be13ae324212bf0bc15d665a4e34844f34c206f78801be42f7a0a8aaa/multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", size = 253584, upload-time = "2025-08-11T12:07:05.914Z" }, - { url = "https://files.pythonhosted.org/packages/5c/74/ab2039ecc05264b5cec73eb018ce417af3ebb384ae9c0e9ed42cb33f8151/multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", size = 251018, upload-time = "2025-08-11T12:07:08.301Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b0/0ed49bba775b135937f52fe13922bc64a7eaf0a3ead84a36e8e4e446e096/multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", size = 263575, upload-time = "2025-08-11T12:07:11.928Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d9/7fb85a85e14de2e44dfb6a24f03c41e2af8697a6df83daddb0e9b7569f73/multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", size = 259649, upload-time = "2025-08-11T12:07:13.244Z" }, { url = "https://files.pythonhosted.org/packages/03/9e/b3a459bcf9b6e74fa461a5222a10ff9b544cb1cd52fd482fb1b75ecda2a2/multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", size = 251505, upload-time = "2025-08-11T12:07:14.57Z" }, { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" }, ] @@ -1995,16 +1949,8 @@ 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/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/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" }, - { url = "https://files.pythonhosted.org/packages/79/22/d1de07632b78ac8e6b785f41fa9aad7a978ec8c0a1bf15772def36d77aac/ninja-1.13.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1c97223cdda0417f414bf864cfb73b72d8777e57ebb279c5f6de368de0062988", size = 179034, upload-time = "2025-08-11T15:09:57.394Z" }, { url = "https://files.pythonhosted.org/packages/ed/de/0e6edf44d6a04dabd0318a519125ed0415ce437ad5a1ec9b9be03d9048cf/ninja-1.13.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa", size = 180716, upload-time = "2025-08-11T15:09:58.696Z" }, - { url = "https://files.pythonhosted.org/packages/54/28/938b562f9057aaa4d6bfbeaa05e81899a47aebb3ba6751e36c027a7f5ff7/ninja-1.13.0-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4be9c1b082d244b1ad7ef41eb8ab088aae8c109a9f3f0b3e56a252d3e00f42c1", size = 146843, upload-time = "2025-08-11T15:10:00.046Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fb/d06a3838de4f8ab866e44ee52a797b5491df823901c54943b2adb0389fbb/ninja-1.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6739d3352073341ad284246f81339a384eec091d9851a886dfa5b00a6d48b3e2", size = 154402, upload-time = "2025-08-11T15:10:01.657Z" }, { url = "https://files.pythonhosted.org/packages/31/bf/0d7808af695ceddc763cf251b84a9892cd7f51622dc8b4c89d5012779f06/ninja-1.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11be2d22027bde06f14c343f01d31446747dbb51e72d00decca2eb99be911e2f", size = 552388, upload-time = "2025-08-11T15:10:03.349Z" }, - { url = "https://files.pythonhosted.org/packages/9d/70/c99d0c2c809f992752453cce312848abb3b1607e56d4cd1b6cded317351a/ninja-1.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aa45b4037b313c2f698bc13306239b8b93b4680eb47e287773156ac9e9304714", size = 472501, upload-time = "2025-08-11T15:10:04.735Z" }, - { url = "https://files.pythonhosted.org/packages/8c/45/9151bba2c8d0ae2b6260f71696330590de5850e5574b7b5694dce6023e20/ninja-1.13.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:3d7d7779d12cb20c6d054c61b702139fd23a7a964ec8f2c823f1ab1b084150db", size = 642420, upload-time = "2025-08-11T15:10:08.35Z" }, - { url = "https://files.pythonhosted.org/packages/3c/fb/95752eb635bb8ad27d101d71bef15bc63049de23f299e312878fc21cb2da/ninja-1.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d741a5e6754e0bda767e3274a0f0deeef4807f1fec6c0d7921a0244018926ae5", size = 585106, upload-time = "2025-08-11T15:10:09.818Z" }, - { url = "https://files.pythonhosted.org/packages/c1/31/aa56a1a286703800c0cbe39fb4e82811c277772dc8cd084f442dd8e2938a/ninja-1.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:e8bad11f8a00b64137e9b315b137d8bb6cbf3086fbdc43bf1f90fd33324d2e96", size = 707138, upload-time = "2025-08-11T15:10:11.366Z" }, { url = "https://files.pythonhosted.org/packages/34/6f/5f5a54a1041af945130abdb2b8529cbef0cdcbbf9bcf3f4195378319d29a/ninja-1.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b4f2a072db3c0f944c32793e91532d8948d20d9ab83da9c0c7c15b5768072200", size = 581758, upload-time = "2025-08-11T15:10:13.295Z" }, ] @@ -2355,11 +2301,8 @@ dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/92/94/01509d510bebf6606614e51113e5a415ced15b8f34aa98a8bf2539314650/openai_harmony-0.0.4.tar.gz", hash = "sha256:5c67ac6df349236fb7b64f57c3dbb0273efcdca24314daa108f2a482c427106c", size = 279848, upload-time = "2025-08-09T01:43:24.974Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/05/88/ade63bd8f36603610040e7cc086bc134d57a99a742e05f7fcddfdf822ee1/openai_harmony-0.0.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf2344366f10981bbc0f6d9949a0b2bb87151d209ed295943ed6ad8eda37932", size = 2963206, upload-time = "2025-08-09T01:43:02.433Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ef/a65a0ff177fdf67bc0afd18bb9e7ad690d1b553a8eb5ebf27f601b22dbd0/openai_harmony-0.0.4-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d8d16d84702059833fb03b841b28c25600c54e83cadccef79af44e1c81166b1", size = 2724854, upload-time = "2025-08-09T01:43:04.606Z" }, - { url = "https://files.pythonhosted.org/packages/45/24/246f6f470bfbc89a117714b68f27cdaee12b31166237a227cc657780cc1d/openai_harmony-0.0.4-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:567cc568b6bf7b4d041b0c9aa7d6b2c9394f8af6065bc87fa6d23f207b5af9a7", size = 3447870, upload-time = "2025-08-09T01:43:06.734Z" }, { url = "https://files.pythonhosted.org/packages/1f/ec/dcdcace0ffcf3a532cca910e0c351b62d3a7decf0b091ea8cf856d2a67a6/openai_harmony-0.0.4-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31e9bcac0902a309e2fc688e52f247eec7fffcd00d17e958b9a83a8fea6519c2", size = 3049306, upload-time = "2025-08-09T01:43:11.019Z" }, { url = "https://files.pythonhosted.org/packages/ad/39/172f1048d935db1523a82b45fee5231ad6c622645e566706e6bcf3731da8/openai_harmony-0.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:96a63199c0d81095b5d5d1ae8ca82b64c1c13d18d4e30323ae9e8ab31bc80a3d", size = 3121347, upload-time = "2025-08-09T01:43:16.705Z" }, - { url = "https://files.pythonhosted.org/packages/6b/36/8ee4ca5d0b25587121fd3621e6a6106fba80218cb6d159e1670aeb2b22ef/openai_harmony-0.0.4-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:d38f2639f6bf7c3c34a5dfd79e29075811ae2fa9b895a63e76767f74a47a971e", size = 2952326, upload-time = "2025-08-09T01:43:18.841Z" }, { url = "https://files.pythonhosted.org/packages/a8/bd/aa9e6e5cf140716dbcae17402fac2a81a9ebb3f934059ac0eec61cb447fc/openai_harmony-0.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:15e6d53a66502491a3675a536df30e271f976e6c5efe68250a65191efcb85c4f", size = 3221129, upload-time = "2025-08-09T01:43:23.146Z" }, ] @@ -2506,12 +2449,8 @@ source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, - { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, - { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, - { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, - { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, ] @@ -2835,7 +2774,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=0760204" }, + { name = "verifiers", editable = "../verifiers" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.14/vllm_router-0.1.14-cp38-abi3-linux_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -2925,13 +2864,8 @@ source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" }, - { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" }, { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" }, { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" }, - { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" }, - { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" }, - { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" }, { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" }, { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, ] @@ -2943,7 +2877,6 @@ source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, ] @@ -3005,15 +2938,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/04/14/43297a7b7f0c1bf0c wheels = [ { 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/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" }, ] @@ -3074,12 +2999,8 @@ dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, @@ -3240,7 +3161,6 @@ source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, @@ -3297,17 +3217,28 @@ source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/81/93/5ab3e899c47fa7994e524447135a71cd121685a35c8fe35029005f8b236f/regex-2026.3.32.tar.gz", hash = "sha256:f1574566457161678297a116fa5d1556c5a4159d64c5ff7c760e7c564bf66f16", size = 415605, upload-time = "2026-03-28T21:49:22.012Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4a/cf/1955bb5567bc491bd63068e17f75ab0c9ff5e9d08466beec7e347f5e768d/regex-2026.3.32-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66a5083c3ffe5a5a95f8281ea47a88072d4f24001d562d1d9d28d4cdc005fec5", size = 796431, upload-time = "2026-03-28T21:46:33.101Z" }, - { url = "https://files.pythonhosted.org/packages/27/8a/67fcbca511b792107540181ee0690df6de877bfbcb41b7ecae7028025ca5/regex-2026.3.32-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e83ce8008b48762be296f1401f19afd9ea29f3d035d1974e0cecb74e9afbd1df", size = 865785, upload-time = "2026-03-28T21:46:35.053Z" }, - { url = "https://files.pythonhosted.org/packages/c2/59/0677bc44f2c28305edcabc11933777b9ad34e9e8ded7ba573d24e4bc3ee7/regex-2026.3.32-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3aa21bad31db904e0b9055e12c8282df62d43169c4a9d2929407060066ebc74", size = 913593, upload-time = "2026-03-28T21:46:36.835Z" }, { url = "https://files.pythonhosted.org/packages/0a/fe/661043d1c263b0d9d10c6ff4e9c9745f3df9641c62b51f96a3473638e7ce/regex-2026.3.32-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f54840bea73541652f1170dc63402a5b776fc851ad36a842da9e5163c1f504a0", size = 801512, upload-time = "2026-03-28T21:46:38.587Z" }, - { url = "https://files.pythonhosted.org/packages/ff/27/74c986061380e1811a46cf04cdf9c939db9f8c0e63953eddfe37ffd633ea/regex-2026.3.32-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2ffbadc647325dd4e3118269bda93ded1eb5f5b0c3b7ba79a3da9fbd04f248e9", size = 776182, upload-time = "2026-03-28T21:46:40.69Z" }, { url = "https://files.pythonhosted.org/packages/b6/c8/d833397b70cd1bacfcdc0a611f0e2c1f5b91fee8eedd88affcee770cbbb6/regex-2026.3.32-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:66d3126afe7eac41759cd5f0b3b246598086e88e70527c0d68c9e615b81771c4", size = 785837, upload-time = "2026-03-28T21:46:42.926Z" }, - { url = "https://files.pythonhosted.org/packages/e0/53/fa226b72989b5b93db6926fab5478115e085dfcf077e18d2cb386be0fd23/regex-2026.3.32-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f785f44a44702dea89b28bce5bc82552490694ce4e144e21a4f0545e364d2150", size = 860612, upload-time = "2026-03-28T21:46:44.8Z" }, - { url = "https://files.pythonhosted.org/packages/04/28/bdd2fc0c055a1b15702bd4084829bbb6b06095f27990e5bee52b2898ea03/regex-2026.3.32-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b7836aa13721dbdef658aebd11f60d00de633a95726521860fe1f6be75fa225a", size = 765285, upload-time = "2026-03-28T21:46:46.625Z" }, - { url = "https://files.pythonhosted.org/packages/b4/da/21f5e2a35a191b27e5a47cccb3914c99e139b49b1342d3f36e64e8cc60f7/regex-2026.3.32-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5336b1506142eb0f23c96fb4a34b37c4fefd4fed2a7042069f3c8058efe17855", size = 851963, upload-time = "2026-03-28T21:46:48.341Z" }, { url = "https://files.pythonhosted.org/packages/18/f4/04ed04ebf335a44083695c22772be6a42efa31900415555563acf02cb4de/regex-2026.3.32-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b56993a7aeb4140c4770f4f7965c9e5af4f024457d06e23c01b0d47501cb18ed", size = 788332, upload-time = "2026-03-28T21:46:50.454Z" }, ] +[[package]] +name = "renderers" +version = "0.1.0" +source = { editable = "../verifiers/packages/renderers" } +dependencies = [ + { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", marker = "extra == 'proxy'", specifier = ">=0.27.0" }, + { name = "starlette", marker = "extra == 'proxy'", specifier = ">=0.40.0" }, + { name = "transformers", specifier = ">=4.50.0" }, + { name = "uvicorn", marker = "extra == 'proxy'", specifier = ">=0.30.0" }, +] +provides-extras = ["proxy"] + [[package]] name = "requests" version = "2.32.5" @@ -3381,12 +3312,8 @@ source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/73/46/05a94dc55ac03cf931d18e43b86ecee5ee054cb88b7853fffd741e35009c/rignore-0.6.4.tar.gz", hash = "sha256:e893fdd2d7fdcfa9407d0b7600ef2c2e2df97f55e1c45d4a8f54364829ddb0ab", size = 11633, upload-time = "2025-07-19T19:24:46.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/75/b9be0c523d97c09f3c6508a67ce376aba4efe41c333c58903a0d7366439a/rignore-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcb0d7d7ecc3fbccf6477bb187c04a091579ea139f15f139abe0b3b48bdfef69", size = 892779, upload-time = "2025-07-19T19:22:35.167Z" }, - { url = "https://files.pythonhosted.org/packages/91/f4/3064b06233697f2993485d132f06fe95061fef71631485da75aed246c4fd/rignore-0.6.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feac73377a156fb77b3df626c76f7e5893d9b4e9e886ac8c0f9d44f1206a2a91", size = 872116, upload-time = "2025-07-19T19:22:47.828Z" }, - { url = "https://files.pythonhosted.org/packages/99/94/cb8e7af9a3c0a665f10e2366144e0ebc66167cf846aca5f1ac31b3661598/rignore-0.6.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:465179bc30beb1f7a3439e428739a2b5777ed26660712b8c4e351b15a7c04483", size = 1163345, upload-time = "2025-07-19T19:23:00.557Z" }, - { url = "https://files.pythonhosted.org/packages/86/6b/49faa7ad85ceb6ccef265df40091d9992232d7f6055fa664fe0a8b13781c/rignore-0.6.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a4877b4dca9cf31a4d09845b300c677c86267657540d0b4d3e6d0ce3110e6e9", size = 939967, upload-time = "2025-07-19T19:23:13.494Z" }, { url = "https://files.pythonhosted.org/packages/80/c8/b91afda10bd5ca1e3a80463340b899c0dc26a7750a9f3c94f668585c7f40/rignore-0.6.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:456456802b1e77d1e2d149320ee32505b8183e309e228129950b807d204ddd17", size = 949717, upload-time = "2025-07-19T19:23:36.404Z" }, { url = "https://files.pythonhosted.org/packages/aa/8f/a80b4a2e48ceba56ba19e096d41263d844757e10aa36ede212571b5d8117/rignore-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e445fbc214ae18e0e644a78086ea5d0f579e210229a4fbe86367d11a4cd03c11", size = 1067837, upload-time = "2025-07-19T19:23:59.888Z" }, - { url = "https://files.pythonhosted.org/packages/7d/90/0905597af0e78748909ef58418442a480ddd93e9fc89b0ca9ab170c357c0/rignore-0.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e07d9c5270fc869bc431aadcfb6ed0447f89b8aafaa666914c077435dc76a123", size = 1134959, upload-time = "2025-07-19T19:24:12.396Z" }, { url = "https://files.pythonhosted.org/packages/4e/a7/92892ed86b2e36da403dd3a0187829f2d880414cef75bd612bfdf4dedebc/rignore-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:536392c5ec91755db48389546c833c4ab1426fe03e5a8522992b54ef8a244e7e", size = 1120546, upload-time = "2025-07-19T19:24:36.377Z" }, ] @@ -3406,11 +3333,7 @@ source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, - { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, - { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, ] @@ -3421,16 +3344,9 @@ version = "0.13.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6e/1a/1f4b722862840295bcaba8c9e5261572347509548faaa99b2d57ee7bfe6a/ruff-0.13.0.tar.gz", hash = "sha256:5b4b1ee7eb35afae128ab94459b13b2baaed282b1fb0f472a73c82c996c8ae60", size = 5372863, upload-time = "2025-09-10T16:25:37.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/fe/6f87b419dbe166fd30a991390221f14c5b68946f389ea07913e1719741e0/ruff-0.13.0-py3-none-linux_armv6l.whl", hash = "sha256:137f3d65d58ee828ae136a12d1dc33d992773d8f7644bc6b82714570f31b2004", size = 12187826, upload-time = "2025-09-10T16:24:39.5Z" }, { url = "https://files.pythonhosted.org/packages/f1/03/8b5ff2a211efb68c63a1d03d157e924997ada87d01bebffbd13a0f3fcdeb/ruff-0.13.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b2c653ae9b9d46e0ef62fc6fbf5b979bda20a0b1d2b22f8f7eb0cde9f4963b8", size = 12312489, upload-time = "2025-09-10T16:24:49.556Z" }, - { url = "https://files.pythonhosted.org/packages/37/fc/2336ef6d5e9c8d8ea8305c5f91e767d795cd4fc171a6d97ef38a5302dadc/ruff-0.13.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cec632534332062bc9eb5884a267b689085a1afea9801bf94e3ba7498a2d207", size = 11991631, upload-time = "2025-09-10T16:24:53.439Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c8/a8a5b81d8729b5d1f663348d11e2a9d65a7a9bd3c399763b1a51c72be1ce/ruff-0.13.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afe37db8e1466acb173bb2a39ca92df00570e0fd7c94c72d87b51b21bb63efea", size = 14697751, upload-time = "2025-09-10T16:24:59.89Z" }, - { url = "https://files.pythonhosted.org/packages/57/f5/183ec292272ce7ec5e882aea74937f7288e88ecb500198b832c24debc6d3/ruff-0.13.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f96a8d90bb258d7d3358b372905fe7333aaacf6c39e2408b9f8ba181f4b6ef2", size = 14095317, upload-time = "2025-09-10T16:25:03.025Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8d/7f9771c971724701af7926c14dab31754e7b303d127b0d3f01116faef456/ruff-0.13.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b5e3d883e4f924c5298e3f2ee0f3085819c14f68d1e5b6715597681433f153", size = 13144418, upload-time = "2025-09-10T16:25:06.272Z" }, { url = "https://files.pythonhosted.org/packages/a8/a6/7985ad1778e60922d4bef546688cd8a25822c58873e9ff30189cfe5dc4ab/ruff-0.13.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03447f3d18479df3d24917a92d768a89f873a7181a064858ea90a804a7538991", size = 13370843, upload-time = "2025-09-10T16:25:09.965Z" }, - { url = "https://files.pythonhosted.org/packages/64/1c/bafdd5a7a05a50cc51d9f5711da704942d8dd62df3d8c70c311e98ce9f8a/ruff-0.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fbc6b1934eb1c0033da427c805e27d164bb713f8e273a024a7e86176d7f462cf", size = 13321891, upload-time = "2025-09-10T16:25:12.969Z" }, { url = "https://files.pythonhosted.org/packages/bc/3e/7817f989cb9725ef7e8d2cee74186bf90555279e119de50c750c4b7a72fe/ruff-0.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8ab6a3e03665d39d4a25ee199d207a488724f022db0e1fe4002968abdb8001b", size = 12119119, upload-time = "2025-09-10T16:25:16.621Z" }, - { url = "https://files.pythonhosted.org/packages/58/07/9df080742e8d1080e60c426dce6e96a8faf9a371e2ce22eef662e3839c95/ruff-0.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2a5c62f8ccc6dd2fe259917482de7275cecc86141ee10432727c4816235bc41", size = 11961594, upload-time = "2025-09-10T16:25:19.49Z" }, { url = "https://files.pythonhosted.org/packages/b6/39/e776c10a3b349fc8209a905bfb327831d7516f6058339a613a8d2aaecacd/ruff-0.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:79ea0c44a3032af768cabfd9616e44c24303af49d633b43e3a5096e009ebe823", size = 13418555, upload-time = "2025-09-10T16:25:25.681Z" }, ] @@ -3441,12 +3357,8 @@ 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" } wheels = [ { 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/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/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" }, ] @@ -3494,9 +3406,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab4 wheels = [ { 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" }, ] @@ -3761,12 +3671,8 @@ dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, - { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, - { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, ] @@ -3832,8 +3738,8 @@ dependencies = [ { name = "typing-extensions", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6f09cdf2415516be028ae82e6b985bcfc3eac37bc52ab401142689f6224516ca" }, - { url = "https://download.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:628e89bd5110ced7debee2a57c69959725b7fbc64eab81a39dd70e46c7e28ba5" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6f09cdf2415516be028ae82e6b985bcfc3eac37bc52ab401142689f6224516ca" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:628e89bd5110ced7debee2a57c69959725b7fbc64eab81a39dd70e46c7e28ba5" }, ] [[package]] @@ -4122,13 +4028,13 @@ wheels = [ [[package]] name = "verifiers" -version = "0.1.12.dev2" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=0760204#07602042d6909b1d3c405d6d99aee0088c7990d2" } +source = { editable = "../verifiers" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "datasets", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "gepa", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "httpx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "jinja2", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "math-verify", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "mcp", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -4142,6 +4048,7 @@ dependencies = [ { name = "pydantic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "pyzmq", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "regex", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "renderers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "requests", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "rich", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "setproctitle", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -4150,6 +4057,75 @@ dependencies = [ { name = "wget", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] +[package.metadata] +requires-dist = [ + { name = "accelerate", marker = "extra == 'rl'", specifier = ">=1.4.0" }, + { name = "aiohttp", marker = "extra == 'browser'", specifier = ">=3.9.0" }, + { name = "aiolimiter", specifier = ">=1.2.1" }, + { name = "anthropic", specifier = ">=0.78.0" }, + { name = "datasets", specifier = ">=3.0.0,<4.7.0" }, + { name = "deepspeed", marker = "extra == 'rl'", specifier = ">=0.17.6" }, + { name = "flash-attn", marker = "extra == 'rl'", specifier = ">=2.8.3" }, + { name = "gepa" }, + { name = "httpx", specifier = ">=0.27.0" }, + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "liger-kernel", marker = "extra == 'rl'", specifier = ">=0.5.10" }, + { name = "math-verify", specifier = ">=0.8.0" }, + { name = "mcp", specifier = ">=1.14.1" }, + { name = "msgpack", specifier = ">=1.1.2" }, + { name = "nest-asyncio", specifier = ">=1.6.0" }, + { name = "nltk", marker = "extra == 'ta'" }, + { name = "numpy" }, + { name = "openai", specifier = ">=1.108.1" }, + { name = "openai-agents", specifier = ">=0.0.7" }, + { name = "openenv-core", extras = ["core"], marker = "extra == 'openenv'", specifier = "==0.2.1" }, + { name = "peft", marker = "extra == 'rl'" }, + { name = "prime-sandboxes", specifier = ">=0.2.19" }, + { name = "prime-tunnel", specifier = ">=0.1.5" }, + { name = "pydantic", specifier = ">=2.11.9" }, + { name = "python-dotenv", marker = "extra == 'browser'", specifier = ">=1.0.0" }, + { name = "pyzmq", specifier = ">=27.1.0" }, + { name = "reasoning-gym", marker = "extra == 'rg'" }, + { name = "regex", specifier = "<2026.4.4" }, + { name = "renderers", editable = "../verifiers/packages/renderers" }, + { name = "requests" }, + { name = "requests", marker = "extra == 'rl'" }, + { name = "rich" }, + { name = "setproctitle", specifier = ">=1.3.0" }, + { name = "stagehand", marker = "extra == 'browser'", specifier = ">=3.0.0" }, + { name = "tenacity", specifier = ">=8.5.0" }, + { name = "textarena", marker = "extra == 'ta'" }, + { name = "textual" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "torch", marker = "extra == 'rl'", specifier = ">=2.8.0,<2.9.0" }, + { name = "transformers", marker = "extra == 'rl'", specifier = ">=4.56.2" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, + { name = "vllm", marker = "extra == 'rl'", specifier = ">=0.10.0,<0.11.0" }, + { name = "wandb", marker = "extra == 'rl'" }, + { name = "wget", specifier = ">=3.2" }, +] +provides-extras = ["browser", "openenv", "rg", "rl", "ta"] + +[package.metadata.requires-dev] +dev = [ + { name = "aiohttp", specifier = ">=3.9.0" }, + { name = "ipykernel" }, + { name = "ipywidgets" }, + { name = "nltk" }, + { name = "openenv-core", extras = ["core"], specifier = "==0.2.1" }, + { name = "pre-commit" }, + { name = "pytest", specifier = ">=7.0.0" }, + { name = "pytest-asyncio", specifier = ">=0.21.0" }, + { name = "pytest-cov", specifier = ">=4.0.0" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "reasoning-gym" }, + { name = "ruff" }, + { name = "stagehand", specifier = ">=3.0.0" }, + { name = "textarena" }, + { name = "ty", specifier = ">=0.0.1a29,<0.0.22" }, +] + [[package]] name = "virtualenv" version = "20.34.0" @@ -4314,9 +4290,6 @@ dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406, upload-time = "2025-06-15T19:06:59.42Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/86/50/dee79968566c03190677c26f7f47960aff738d32087087bdf63a5473e7df/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297", size = 450939, upload-time = "2025-06-15T19:05:26.494Z" }, - { url = "https://files.pythonhosted.org/packages/40/45/a7b56fb129700f3cfe2594a01aa38d033b92a33dddce86c8dfdfc1247b72/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0", size = 457270, upload-time = "2025-06-15T19:05:27.466Z" }, - { url = "https://files.pythonhosted.org/packages/98/68/42cfcdd6533ec94f0a7aab83f759ec11280f70b11bfba0b0f885e298f9bd/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee", size = 598654, upload-time = "2025-06-15T19:05:29.997Z" }, - { url = "https://files.pythonhosted.org/packages/d3/74/b2a1544224118cc28df7e59008a929e711f9c68ce7d554e171b2dc531352/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd", size = 478667, upload-time = "2025-06-15T19:05:31.172Z" }, { url = "https://files.pythonhosted.org/packages/8c/77/e3362fe308358dc9f8588102481e599c83e1b91c2ae843780a7ded939a35/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f", size = 452213, upload-time = "2025-06-15T19:05:32.299Z" }, { url = "https://files.pythonhosted.org/packages/6e/17/c8f1a36540c9a1558d4faf08e909399e8133599fa359bf52ec8fcee5be6f/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4", size = 626718, upload-time = "2025-06-15T19:05:33.415Z" }, { url = "https://files.pythonhosted.org/packages/26/45/fb599be38b4bd38032643783d7496a26a6f9ae05dea1a42e58229a20ac13/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f", size = 623098, upload-time = "2025-06-15T19:05:34.534Z" }, @@ -4419,12 +4392,8 @@ source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/00/5e/d6e5258d69df8b4ed8c83b6664f2b47d30d2dec551a29ad72a6c69eafd31/xxhash-3.5.0.tar.gz", hash = "sha256:84f2caddf951c9cbf8dc2e22a89d4ccf5d86391ac6418fe81e3c67d0cf60b45f", size = 84241, upload-time = "2024-08-17T09:20:38.972Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5b/84/de7c89bc6ef63d750159086a6ada6416cc4349eab23f76ab870407178b93/xxhash-3.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08424f6648526076e28fae6ea2806c0a7d504b9ef05ae61d196d571e5c879c84", size = 220959, upload-time = "2024-08-17T09:18:26.518Z" }, - { url = "https://files.pythonhosted.org/packages/fe/86/51258d3e8a8545ff26468c977101964c14d56a8a37f5835bc0082426c672/xxhash-3.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61a1ff00674879725b194695e17f23d3248998b843eb5e933007ca743310f793", size = 200006, upload-time = "2024-08-17T09:18:27.905Z" }, - { url = "https://files.pythonhosted.org/packages/02/0a/96973bd325412feccf23cf3680fd2246aebf4b789122f938d5557c54a6b2/xxhash-3.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f2c61bee5844d41c3eb015ac652a0229e901074951ae48581d58bfb2ba01be", size = 428326, upload-time = "2024-08-17T09:18:29.335Z" }, { url = "https://files.pythonhosted.org/packages/11/a7/81dba5010f7e733de88af9555725146fc133be97ce36533867f4c7e75066/xxhash-3.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d32a592cac88d18cc09a89172e1c32d7f2a6e516c3dfde1b9adb90ab5df54a6", size = 194380, upload-time = "2024-08-17T09:18:30.706Z" }, { url = "https://files.pythonhosted.org/packages/8a/6e/6e88b8f24612510e73d4d70d9b0c7dff62a2e78451b9f0d042a5462c8d03/xxhash-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e5d0ddaca65ecca9c10dcf01730165fd858533d0be84c75c327487c37a906a27", size = 216301, upload-time = "2024-08-17T09:18:33.474Z" }, - { url = "https://files.pythonhosted.org/packages/22/61/8d6a40f288f791cf79ed5bb113159abf0c81d6efb86e734334f698eb4c59/xxhash-3.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149b7914451eb154b3dfaa721315117ea1dac2cc55a01bfbd4df7c68c5dd683d", size = 210294, upload-time = "2024-08-17T09:18:36.355Z" }, - { url = "https://files.pythonhosted.org/packages/17/02/215c4698955762d45a8158117190261b2dbefe9ae7e5b906768c09d8bc74/xxhash-3.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:eade977f5c96c677035ff39c56ac74d851b1cca7d607ab3d8f23c6b859379cab", size = 414674, upload-time = "2024-08-17T09:18:38.536Z" }, { url = "https://files.pythonhosted.org/packages/31/5c/b7a8db8a3237cff3d535261325d95de509f6a8ae439a5a7a4ffcff478189/xxhash-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa9f547bd98f5553d03160967866a71056a60960be00356a15ecc44efb40ba8e", size = 192022, upload-time = "2024-08-17T09:18:40.138Z" }, ] @@ -4440,14 +4409,8 @@ dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" }, - { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" }, - { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" }, - { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" }, { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" }, { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" }, - { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" }, - { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" }, { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" }, { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, ] From 63b61e96c00dcd95ef83d8c7b4e8c502b5b86237 Mon Sep 17 00:00:00 2001 From: hallerite Date: Wed, 8 Apr 2026 09:19:11 +0000 Subject: [PATCH 02/54] feat: remove token-client training path --- docs/on_policy_distillation.md | 1 - examples/alphabet_sort/sft_distill_hard.toml | 1 - skills/entrypoints/SKILL.md | 12 +- src/prime_rl/configs/orchestrator.py | 7 - src/prime_rl/configs/rl.py | 5 - src/prime_rl/configs/shared.py | 3 +- src/prime_rl/inference/vllm/server.py | 49 +---- .../vllm/serving_chat_with_tokens.py | 180 +----------------- .../inference/vllm/serving_generate.py | 35 +++- src/prime_rl/orchestrator/orchestrator.py | 12 +- src/prime_rl/orchestrator/utils.py | 14 +- src/prime_rl/trainer/model.py | 21 ++ tests/unit/inference/test_serving_generate.py | 83 ++++++++ .../orchestrator/test_teacher_logprobs.py | 73 +++++++ tests/unit/train/test_model_forward.py | 50 +++++ 15 files changed, 297 insertions(+), 249 deletions(-) create mode 100644 tests/unit/inference/test_serving_generate.py create mode 100644 tests/unit/orchestrator/test_teacher_logprobs.py create mode 100644 tests/unit/train/test_model_forward.py diff --git a/docs/on_policy_distillation.md b/docs/on_policy_distillation.md index 093ccfec710..10699c520e5 100644 --- a/docs/on_policy_distillation.md +++ b/docs/on_policy_distillation.md @@ -71,7 +71,6 @@ Use this mode when you want to train from teacher-generated completions directly type = "sft" [orchestrator] -use_token_client = false [orchestrator.teacher_rollout_model.client] base_url = ["https://your-openai-compatible-endpoint/v1"] diff --git a/examples/alphabet_sort/sft_distill_hard.toml b/examples/alphabet_sort/sft_distill_hard.toml index 420a2bec4bd..87c9559d48c 100644 --- a/examples/alphabet_sort/sft_distill_hard.toml +++ b/examples/alphabet_sort/sft_distill_hard.toml @@ -32,7 +32,6 @@ save_adapter_separately = true [orchestrator] batch_size = 256 rollouts_per_example = 4 -use_token_client = false [orchestrator.sampling] max_tokens = 512 diff --git a/skills/entrypoints/SKILL.md b/skills/entrypoints/SKILL.md index f6589aca976..2e00ec28ccd 100644 --- a/skills/entrypoints/SKILL.md +++ b/skills/entrypoints/SKILL.md @@ -17,6 +17,16 @@ uv run rl @ examples/reverse_text/rl.toml @ examples/reverse_text/slurm_rl.toml uv run rl @ examples/reverse_text/rl.toml --dry-run # generate scripts without running ``` +When running from a fresh worktree, include optional extras required by the config. For example, VLM color-codeword debugging needs FlashAttention and the env packages: + +```bash +uv run --extra flash-attn --extra envs rl @ configs/multimodal/rl_color_codeword_test.toml +``` + +Short debug runs can write large filesystem weight-broadcast snapshots. If `/` is low on space, put the debug `--output-dir` on `/dev/shm` and clean it up afterward. + +For ad-hoc `CliAgentEnv`/`ComposableEnv` sandbox smoke tests that call back through Prime Tunnel using Python stdlib `urllib`, set an explicit non-default `User-Agent` header such as `curl/7.81.0`. The tunnel can reject Python's default `urllib` user agent from inside Prime sandboxes with HTTP 403 / code 1010. Prefer uploading an agent script into the sandbox and running `python3 /tmp/agent.py` over passing a heredoc directly as a `start_background_job` command. + - **Config:** `RLConfig` (`src/prime_rl/configs/rl.py`) - **Entrypoint:** `src/prime_rl/entrypoints/rl.py` - **SLURM:** yes — single-node and multi-node @@ -49,7 +59,7 @@ uv run inference --model.name Qwen/Qwen3-0.6B --model.enforce-eager Always use the `inference` entrypoint — never `vllm serve` directly. Custom endpoints beyond standard OpenAI API: -- `/v1/chat/completions/tokens` — accepts token IDs as prompt input +- `/v1/generate` — accepts token IDs as prompt input, with optional images - `/update_weights` — hot-reload model weights from the trainer - `/load_lora_adapter` — load LoRA adapters at runtime - `/init_broadcaster` — initialize weight broadcast for distributed training diff --git a/src/prime_rl/configs/orchestrator.py b/src/prime_rl/configs/orchestrator.py index 89127bda111..d6f2da9302d 100644 --- a/src/prime_rl/configs/orchestrator.py +++ b/src/prime_rl/configs/orchestrator.py @@ -932,13 +932,6 @@ class OrchestratorConfig(BaseConfig): HeartbeatConfig | None, Field(description="The heartbeat config for monitoring training progress.") ] = None - use_token_client: Annotated[ - bool, - Field( - description="Whether to use the token-in-token-out (TITO) client for training across all environments. WARNING: Only use this if your environment has a linear history and the chat template has the extension property (i.e. no tokens are ever removed or inserted by the chat template)" - ), - ] = True - @model_validator(mode="after") def validate_unique_filter_types(self): types = [f.type for f in self.filters] diff --git a/src/prime_rl/configs/rl.py b/src/prime_rl/configs/rl.py index d370a175b2a..76648538dc2 100644 --- a/src/prime_rl/configs/rl.py +++ b/src/prime_rl/configs/rl.py @@ -417,11 +417,6 @@ def validate_external_rollout_mode(self): "External rollout mode does not use the local inference server." ) - if self.orchestrator.use_token_client: - raise ValueError( - "orchestrator.use_token_client must be false when orchestrator.teacher_rollout_model is configured." - ) - return self ### Auto-setup and validate shared configs diff --git a/src/prime_rl/configs/shared.py b/src/prime_rl/configs/shared.py index 2b32d4a2649..259eef61612 100644 --- a/src/prime_rl/configs/shared.py +++ b/src/prime_rl/configs/shared.py @@ -128,7 +128,8 @@ class BaseModelConfig(BaseConfig): Field( description=( "Renderer to use for chat template tokenization. " - "Options: 'auto' (detect from tokenizer), 'qwen3', 'qwen3.5', 'glm5', 'glm4.5', 'minimax-m2', 'default'." + "Options: 'auto' (detect from tokenizer), 'qwen3', 'qwen3_vl', " + "'qwen3.5', 'glm5', 'glm4.5', 'minimax-m2', 'default'." ), ), ] = "auto" diff --git a/src/prime_rl/inference/vllm/server.py b/src/prime_rl/inference/vllm/server.py index dd1e845a49e..0c523e94623 100644 --- a/src/prime_rl/inference/vllm/server.py +++ b/src/prime_rl/inference/vllm/server.py @@ -1,19 +1,16 @@ from argparse import Namespace -from http import HTTPStatus from typing import Any import uvloop -from fastapi import APIRouter, Depends, HTTPException, Request -from fastapi.responses import JSONResponse, StreamingResponse +from fastapi import APIRouter, Request +from fastapi.responses import JSONResponse from starlette.datastructures import State from vllm.engine.protocol import EngineClient from vllm.entrypoints.openai.api_server import init_app_state -from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionResponse from vllm.entrypoints.openai.cli_args import make_arg_parser, validate_parsed_serve_args from vllm.entrypoints.openai.engine.protocol import ErrorResponse from vllm.entrypoints.openai.engine.serving import OpenAIServing from vllm.entrypoints.openai.models.serving import OpenAIServingModels -from vllm.entrypoints.openai.utils import validate_json_request from vllm.entrypoints.serve.lora.protocol import LoadLoRAAdapterRequest from vllm.entrypoints.utils import load_aware_call, with_cancellation from vllm.logger import init_logger @@ -137,7 +134,6 @@ def resolve_tool_call_parser(model_name: str, tool_call_parser: str | None) -> s monkey_patch_tokenize_params_validation, ) from prime_rl.inference.vllm.serving_chat_with_tokens import ( - ChatCompletionRequestWithTokens, OpenAIServingChatWithTokens, ) @@ -175,10 +171,6 @@ def models(request: Request) -> OpenAIServingModels: } -def chat_with_tokens(request: Request) -> OpenAIServingChatWithTokens | None: - return request.app.state.openai_serving_chat_with_tokens - - def generate_handler(request: Request): return request.app.state.openai_serving_generate @@ -253,35 +245,6 @@ async def init_broadcaster(request: Request): return {"status": "ok"} -@router.post( - "/v1/chat/completions/tokens", - dependencies=[Depends(validate_json_request)], - responses={ - HTTPStatus.OK.value: {"content": {"text/event-stream": {}}}, - HTTPStatus.BAD_REQUEST.value: {"model": ErrorResponse}, - HTTPStatus.NOT_FOUND.value: {"model": ErrorResponse}, - HTTPStatus.INTERNAL_SERVER_ERROR.value: {"model": ErrorResponse}, - }, -) -@with_cancellation -@load_aware_call -async def _chat_with_tokens(request: ChatCompletionRequestWithTokens, raw_request: Request): - handler = chat_with_tokens(raw_request) - if handler is None: - return base(raw_request).create_error_response(message="The model does not support Chat Completions API") - try: - generator = await handler.create_chat_completion_with_tokens(request, raw_request) - except Exception as e: - raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value, detail=str(e)) from e - if isinstance(generator, ErrorResponse): - return JSONResponse(content=generator.model_dump(), status_code=generator.error.code) - - elif isinstance(generator, ChatCompletionResponse): - return JSONResponse(content=generator.model_dump()) - - return StreamingResponse(content=generator, media_type="text/event-stream") - - async def custom_init_app_state( engine_client: EngineClient, state: State, @@ -291,7 +254,8 @@ async def custom_init_app_state( """ Modifies init_app_state: 1. Call the original init_app_state to set up standard state. - 2. Replace the serving_chat with our OpenAIServingChatWithTokens wrapper. + 2. Replace the serving_chat with our wrapper so chat responses can still + expose routed experts when requested. """ await init_app_state(engine_client, state, args, supported_tasks) @@ -300,14 +264,13 @@ async def custom_init_app_state( serving_chat = object.__new__(OpenAIServingChatWithTokens) serving_chat.__dict__.update(original_chat.__dict__) state.openai_serving_chat = serving_chat - state.openai_serving_chat_with_tokens = serving_chat else: - state.openai_serving_chat_with_tokens = None + serving_chat = None # /v1/generate endpoint — tokens + optional images, no chat template from prime_rl.inference.vllm.serving_generate import OpenAIServingGenerate - state.openai_serving_generate = OpenAIServingGenerate(engine_client) + state.openai_serving_generate = OpenAIServingGenerate(engine_client, chat_handler=serving_chat) import vllm.entrypoints.openai.api_server diff --git a/src/prime_rl/inference/vllm/serving_chat_with_tokens.py b/src/prime_rl/inference/vllm/serving_chat_with_tokens.py index e7f4b911f64..78f06aa7461 100644 --- a/src/prime_rl/inference/vllm/serving_chat_with_tokens.py +++ b/src/prime_rl/inference/vllm/serving_chat_with_tokens.py @@ -1,20 +1,12 @@ import base64 from collections.abc import AsyncGenerator, AsyncIterator -from typing import ClassVar, Optional, Union import numpy as np -from fastapi import Request -from pydantic import Field from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest, ChatCompletionResponse from vllm.entrypoints.openai.chat_completion.serving import OpenAIServingChat -from vllm.entrypoints.openai.engine.protocol import ErrorResponse, RequestResponseMetadata -from vllm.entrypoints.openai.engine.serving import GenerationError -from vllm.entrypoints.utils import get_max_tokens -from vllm.exceptions import VLLMValidationError +from vllm.entrypoints.openai.engine.protocol import ErrorResponse from vllm.logger import init_logger from vllm.outputs import RequestOutput -from vllm.reasoning import ReasoningParser -from vllm.sampling_params import BeamSearchParams, SamplingParams logger = init_logger(__name__) @@ -43,11 +35,6 @@ def post_process(self, response: ChatCompletionResponse): choice.routed_experts = self.routed_experts[choice.index] -class ChatCompletionRequestWithTokens(ChatCompletionRequest): - field_names: ClassVar[Optional[set[str]]] = None - tokens: list[int] = Field(description=("Prompt tokens to use for the request.")) - - class OpenAIServingChatWithTokens(OpenAIServingChat): """OpenAI-compatible generate API that allows token-in and routed experts capture.""" @@ -89,168 +76,3 @@ async def chat_completion_full_generator( capture.post_process(response) return response - - async def create_chat_completion_with_tokens( - self, - request: ChatCompletionRequestWithTokens, - raw_request: Optional[Request] = None, - ) -> Union[AsyncGenerator[str, None], ChatCompletionResponse, ErrorResponse]: - """ - Chat Completion API similar to OpenAI's API. - - See https://platform.openai.com/docs/api-reference/chat/create - for the API specification. This API mimics the OpenAI - Chat Completion API. - """ - # Streaming response - tokenizer = self.renderer.tokenizer - assert tokenizer is not None - reasoning_parser: ReasoningParser | None = None - try: - if self.reasoning_parser_cls: - # Pass the same chat template kwargs as used in tokenization - chat_template_kwargs = self._prepare_extra_chat_template_kwargs( - request.chat_template_kwargs, - self.default_chat_template_kwargs, - ) - reasoning_parser = self.reasoning_parser_cls( - tokenizer, - chat_template_kwargs=chat_template_kwargs, # type: ignore[call-arg] - ) - except RuntimeError as e: - logger.exception("Error in reasoning parser creation.") - return self.create_error_response(str(e)) - result = await self.render_chat_request(request) - if isinstance(result, ErrorResponse): - return result - - conversation, engine_prompts = result - - # We override prompt tokens directly. - # VLM conversations use MITO (message-based) instead of TITO, so - # multi_modal_data is not expected here. - engine_prompts[0]["prompt_token_ids"] = request.tokens # type: ignore - - request_id = f"chatcmpl-{self._base_request_id(raw_request, request.request_id)}" - - request_metadata = RequestResponseMetadata(request_id=request_id) - if raw_request: - raw_request.state.request_metadata = request_metadata - - try: - lora_request = self._maybe_get_adapters(request, supports_default_mm_loras=True) - - model_name = self.models.model_name(lora_request) - except (ValueError, TypeError, RuntimeError) as e: - logger.exception("Error preparing request components") - return self.create_error_response(e) - - # Extract data_parallel_rank from header (router can inject it) - data_parallel_rank = self._get_data_parallel_rank(raw_request) - - # Schedule the request and get the result generator. - max_model_len = self.model_config.max_model_len - generators: list[AsyncGenerator[RequestOutput, None]] = [] - try: - for i, engine_prompt in enumerate(engine_prompts): - prompt_token_ids = self._extract_prompt_components(engine_prompt).token_ids - - # If we are creating sub requests for multiple prompts, ensure that they - # have unique request ids. - sub_request_id = request_id if len(engine_prompts) == 1 else f"{request_id}_{i}" - - prompt_len = self._extract_prompt_len(engine_prompt) - if prompt_len >= max_model_len: - raise VLLMValidationError( - f"This model's maximum context length is " - f"{max_model_len} tokens. However, your request has " - f"{prompt_len} input tokens. Please reduce the length of " - "the input messages.", - parameter="input_tokens", - value=prompt_len, - ) - - max_tokens = get_max_tokens( - max_model_len, - request.max_completion_tokens if request.max_completion_tokens is not None else request.max_tokens, - self._extract_prompt_len(engine_prompt), - self.default_sampling_params, - self.override_max_tokens, - ) - - sampling_params: SamplingParams | BeamSearchParams - if request.use_beam_search: - sampling_params = request.to_beam_search_params(max_tokens, self.default_sampling_params) - else: - sampling_params = request.to_sampling_params( - max_tokens, - self.default_sampling_params, - ) - - self._log_inputs( - sub_request_id, - engine_prompt, - params=sampling_params, - lora_request=lora_request, - ) - - trace_headers = None if raw_request is None else await self._get_trace_headers(raw_request.headers) - - if isinstance(sampling_params, BeamSearchParams): - generator = self.beam_search( - prompt=engine_prompt, - request_id=sub_request_id, - params=sampling_params, - lora_request=lora_request, - trace_headers=trace_headers, - ) - else: - reasoning_ended = ( - reasoning_parser.is_reasoning_end(prompt_token_ids or []) if reasoning_parser else None - ) - - generator = self.engine_client.generate( - engine_prompt, - sampling_params, - sub_request_id, - lora_request=lora_request, - trace_headers=trace_headers, - priority=request.priority, - data_parallel_rank=data_parallel_rank, - reasoning_ended=reasoning_ended, - ) - - generators.append(generator) - except ValueError as e: - return self.create_error_response(e) - - assert len(generators) == 1 - (result_generator,) = generators - - if request.stream: - return self.chat_completion_stream_generator( - request, - result_generator, - request_id, - model_name, - conversation, - tokenizer, - request_metadata, - reasoning_parser, - ) - - try: - return await self.chat_completion_full_generator( - request, - result_generator, - request_id, - model_name, - conversation, - tokenizer, - request_metadata, - reasoning_parser, - ) - except GenerationError: - raise # Let FastAPI's global generation_error_handler handle it - except ValueError as e: - return self.create_error_response(e) diff --git a/src/prime_rl/inference/vllm/serving_generate.py b/src/prime_rl/inference/vllm/serving_generate.py index b5cec8f156b..83c86ee0f45 100644 --- a/src/prime_rl/inference/vllm/serving_generate.py +++ b/src/prime_rl/inference/vllm/serving_generate.py @@ -11,6 +11,7 @@ from __future__ import annotations import base64 +from collections.abc import Mapping from io import BytesIO from typing import Any from uuid import uuid4 @@ -52,6 +53,7 @@ class GenerateRequest(BaseModel): stop_token_ids: list[int] | None = None repetition_penalty: float = 1.0 min_tokens: int = 0 + prompt_logprobs: bool = False class GenerateChoiceResponse(BaseModel): @@ -68,6 +70,7 @@ class GenerateResponse(BaseModel): prompt_token_ids: list[int] choices: list[GenerateChoiceResponse] usage: dict + prompt_logprobs: list[float | None] | None = None # ── Handler ────────────────────────────────────────────────────────── @@ -76,8 +79,9 @@ class GenerateResponse(BaseModel): class OpenAIServingGenerate: """Lightweight generate handler — tokens + optional images in, tokens out.""" - def __init__(self, engine_client: EngineClient): + def __init__(self, engine_client: EngineClient, chat_handler: Any | None = None): self.engine_client = engine_client + self.chat_handler = chat_handler async def generate(self, request: GenerateRequest, raw_request: Request) -> GenerateResponse | dict: engine_prompt: dict[str, Any] = { @@ -100,17 +104,25 @@ async def generate(self, request: GenerateRequest, raw_request: Request) -> Gene repetition_penalty=request.repetition_penalty, min_tokens=request.min_tokens, logprobs=1, + prompt_logprobs=1 if request.prompt_logprobs else None, skip_special_tokens=False, ) request_id = f"gen-{uuid4().hex[:16]}" routed_experts_map: dict[int, dict] = {} final_output: RequestOutput | None = None + data_parallel_rank = None + trace_headers = None + if self.chat_handler is not None: + data_parallel_rank = self.chat_handler._get_data_parallel_rank(raw_request) + trace_headers = await self.chat_handler._get_trace_headers(raw_request.headers) generator = self.engine_client.generate( engine_prompt, sampling_params, request_id, + trace_headers=trace_headers, + data_parallel_rank=data_parallel_rank, ) async for output in generator: @@ -148,6 +160,7 @@ async def generate(self, request: GenerateRequest, raw_request: Request) -> Gene prompt_len = len(final_output.prompt_token_ids) completion_len = sum(len(c.token_ids) for c in choices) + prompt_logprobs = _extract_prompt_logprobs(final_output.prompt_logprobs) return GenerateResponse( id=request_id, @@ -159,6 +172,7 @@ async def generate(self, request: GenerateRequest, raw_request: Request) -> Gene "completion_tokens": completion_len, "total_tokens": prompt_len + completion_len, }, + prompt_logprobs=prompt_logprobs, ) @@ -167,3 +181,22 @@ def _encode_routed_experts(arr: np.ndarray) -> dict: "data": base64.b85encode(arr.tobytes()).decode("ascii"), "shape": list(arr.shape), } + + +def _extract_prompt_logprobs( + prompt_logprobs: list[dict[int, Any] | None] | Mapping[int, Any] | None, +) -> list[float | None] | None: + if prompt_logprobs is None: + return None + if isinstance(prompt_logprobs, Mapping): + prompt_logprobs = [prompt_logprobs] + + extracted: list[float | None] = [] + for token_logprobs in prompt_logprobs: + if not token_logprobs: + extracted.append(None) + continue + selected = next(iter(token_logprobs.values())) + logprob = selected.logprob if hasattr(selected, "logprob") else selected.get("logprob") + extracted.append(float(logprob) if logprob is not None else None) + return extracted diff --git a/src/prime_rl/orchestrator/orchestrator.py b/src/prime_rl/orchestrator/orchestrator.py index 253fe7cf430..a0a0245f545 100644 --- a/src/prime_rl/orchestrator/orchestrator.py +++ b/src/prime_rl/orchestrator/orchestrator.py @@ -4,6 +4,7 @@ import multiprocessing as mp import time from concurrent.futures import ThreadPoolExecutor +from contextlib import suppress import tomli_w @@ -156,7 +157,7 @@ async def orchestrate(config: OrchestratorConfig): proxy = RenderingProxy(renderer, vllm_base_url=rollout_client_config.base_url[0], processor=processor) proxy_port = 18100 proxy_server = uvicorn.Server(uvicorn.Config(proxy.app, host="127.0.0.1", port=proxy_port, log_level="warning")) - asyncio.create_task(proxy_server.serve()) + proxy_server_task = asyncio.create_task(proxy_server.serve()) logger.info(f"Started rendering proxy on port {proxy_port} → {rollout_client_config.base_url[0]}") from copy import deepcopy @@ -926,6 +927,15 @@ def compute_solve_rates(df): # Stop inference pool await inference_pool.stop() + proxy_server.should_exit = True + try: + await asyncio.wait_for(proxy_server_task, timeout=5) + except TimeoutError: + proxy_server_task.cancel() + with suppress(asyncio.CancelledError): + await proxy_server_task + await proxy.close() + if teacher_inference_pool is not None: await teacher_inference_pool.stop() diff --git a/src/prime_rl/orchestrator/utils.py b/src/prime_rl/orchestrator/utils.py index 5c6f56a82fa..af4a02a8274 100644 --- a/src/prime_rl/orchestrator/utils.py +++ b/src/prime_rl/orchestrator/utils.py @@ -155,29 +155,25 @@ async def compute_teacher_logprobs( samples: list[TrainingSample], ) -> list[list[float]]: """Compute teacher model logprobs for a batch of training samples via prefill.""" + from prime_rl.inference.vllm.serving_generate import GenerateResponse async def _compute_single(client_config: vf.ClientConfig, sample: TrainingSample) -> list[float]: client = setup_openai_client(client_config) async with await get_semaphore(): response = await client.post( - "/chat/completions/tokens", + "/generate", + cast_to=GenerateResponse, body={ "model": model_name, - "messages": [{"role": "user", "content": ""}], - "tokens": sample.prompt_ids + sample.completion_ids, + "prompt_token_ids": sample.prompt_ids + sample.completion_ids, "max_tokens": 1, "temperature": 1.0, "top_p": 1.0, - "skip_special_tokens": False, "prompt_logprobs": True, }, - cast_to=ChatCompletion, ) - return [ - 0.0 if lp is None else float(next(iter(lp.values()))["logprob"]) - for lp in getattr(response, "prompt_logprobs", []) - ] + return [0.0 if lp is None else float(lp) for lp in response.prompt_logprobs or []] return await asyncio.gather(*[_compute_single(client, sample) for client, sample in zip(cycle(clients), samples)]) diff --git a/src/prime_rl/trainer/model.py b/src/prime_rl/trainer/model.py index 188b0f61f7e..05d59c3fcd3 100644 --- a/src/prime_rl/trainer/model.py +++ b/src/prime_rl/trainer/model.py @@ -835,6 +835,24 @@ def setup_model( return model +def _get_qwen3_vl_mm_token_type_ids(model: nn.Module, input_ids: Tensor) -> Tensor | None: + config = getattr(model, "config", None) + if getattr(config, "model_type", None) != "qwen3_vl": + return None + + mm_token_type_ids = torch.zeros_like(input_ids) + + image_token_id = getattr(config, "image_token_id", None) + if image_token_id is not None: + mm_token_type_ids = mm_token_type_ids.masked_fill(input_ids == image_token_id, 1) + + video_token_id = getattr(config, "video_token_id", None) + if video_token_id is not None: + mm_token_type_ids = mm_token_type_ids.masked_fill(input_ids == video_token_id, 2) + + return mm_token_type_ids + + @jaxtyped(typechecker=typechecker) def forward( model: nn.Module, @@ -860,6 +878,9 @@ def forward( assert image_grid_thw is not None, "pixel_values requires image_grid_thw for MRoPE computation" kwargs["pixel_values"] = pixel_values kwargs["image_grid_thw"] = image_grid_thw + mm_token_type_ids = _get_qwen3_vl_mm_token_type_ids(model, input_ids) + if mm_token_type_ids is not None: + kwargs["mm_token_type_ids"] = mm_token_type_ids else: kwargs["position_ids"] = position_ids diff --git a/tests/unit/inference/test_serving_generate.py b/tests/unit/inference/test_serving_generate.py new file mode 100644 index 00000000000..508682b6bdf --- /dev/null +++ b/tests/unit/inference/test_serving_generate.py @@ -0,0 +1,83 @@ +import asyncio + +from vllm.logprobs import Logprob +from vllm.outputs import CompletionOutput, RequestOutput + +from prime_rl.inference.vllm.serving_generate import GenerateRequest, OpenAIServingGenerate + + +class _FakeChatHandler: + def _get_data_parallel_rank(self, raw_request): + assert raw_request.headers["X-data-parallel-rank"] == "3" + return 3 + + async def _get_trace_headers(self, headers): + assert headers["traceparent"] == "trace" + return {"traceparent": headers["traceparent"]} + + +class _FakeEngineClient: + def __init__(self): + self.calls = [] + + async def generate(self, engine_prompt, sampling_params, request_id, **kwargs): + self.calls.append( + { + "engine_prompt": engine_prompt, + "sampling_params": sampling_params, + "request_id": request_id, + "kwargs": kwargs, + } + ) + yield RequestOutput( + request_id=request_id, + prompt=None, + prompt_token_ids=list(engine_prompt["prompt_token_ids"]), + prompt_logprobs=[None, {11: Logprob(-0.2)}, {12: Logprob(-0.4)}], + outputs=[ + CompletionOutput( + index=0, + text="done", + token_ids=[99], + cumulative_logprob=-0.6, + logprobs=[{99: Logprob(-0.6)}], + finish_reason="stop", + ) + ], + finished=True, + ) + + +class _FakeRawRequest: + def __init__(self): + self.headers = {"X-data-parallel-rank": "3", "traceparent": "trace"} + + async def is_disconnected(self): + return False + + +def test_generate_returns_prompt_logprobs_and_forwards_request_metadata(): + async def _run(): + engine_client = _FakeEngineClient() + handler = OpenAIServingGenerate(engine_client, chat_handler=_FakeChatHandler()) + request = GenerateRequest( + model="test-model", + prompt_token_ids=[10, 11, 12], + max_tokens=1, + prompt_logprobs=True, + ) + + response = await handler.generate(request, _FakeRawRequest()) + + assert response.prompt_token_ids == [10, 11, 12] + assert response.prompt_logprobs == [None, -0.2, -0.4] + assert response.choices[0].token_ids == [99] + assert response.choices[0].logprobs == [-0.6] + + call = engine_client.calls[0] + assert call["engine_prompt"] == {"prompt_token_ids": [10, 11, 12]} + assert call["sampling_params"].prompt_logprobs == 1 + assert call["kwargs"]["data_parallel_rank"] == 3 + assert call["kwargs"]["trace_headers"] == {"traceparent": "trace"} + + asyncio.run(_run()) diff --git a/tests/unit/orchestrator/test_teacher_logprobs.py b/tests/unit/orchestrator/test_teacher_logprobs.py new file mode 100644 index 00000000000..8cb94c5eb11 --- /dev/null +++ b/tests/unit/orchestrator/test_teacher_logprobs.py @@ -0,0 +1,73 @@ +import asyncio +from contextlib import asynccontextmanager + +import verifiers as vf + +from prime_rl.inference.vllm.serving_generate import GenerateResponse +from prime_rl.orchestrator import utils as orchestrator_utils +from prime_rl.transport import TrainingSample + + +class _FakeOpenAIClient: + def __init__(self): + self.calls = [] + + async def post(self, path, *, cast_to, body): + self.calls.append({"path": path, "cast_to": cast_to, "body": body}) + return GenerateResponse( + id="gen-test", + model=body["model"], + prompt_token_ids=list(body["prompt_token_ids"]), + choices=[], + usage={"prompt_tokens": 3, "completion_tokens": 0, "total_tokens": 3}, + prompt_logprobs=[None, -0.7, -0.3], + ) + + +@asynccontextmanager +async def _null_semaphore(): + yield + + +def test_compute_teacher_logprobs_uses_generate_endpoint(monkeypatch): + async def _run(): + fake_client = _FakeOpenAIClient() + monkeypatch.setattr(orchestrator_utils, "setup_openai_client", lambda _: fake_client) + + async def _fake_get_semaphore(): + return _null_semaphore() + + monkeypatch.setattr(orchestrator_utils, "get_semaphore", _fake_get_semaphore) + + sample = TrainingSample( + prompt_ids=[1], + prompt_mask=[True], + completion_ids=[2, 3], + completion_mask=[True, True], + completion_logprobs=[-0.1, -0.2], + completion_temperatures=[1.0, 1.0], + ) + + result = await orchestrator_utils.compute_teacher_logprobs( + clients=[vf.ClientConfig()], + model_name="teacher-model", + samples=[sample], + ) + + assert result == [[0.0, -0.7, -0.3]] + assert fake_client.calls == [ + { + "path": "/generate", + "cast_to": GenerateResponse, + "body": { + "model": "teacher-model", + "prompt_token_ids": [1, 2, 3], + "max_tokens": 1, + "temperature": 1.0, + "top_p": 1.0, + "prompt_logprobs": True, + }, + } + ] + + asyncio.run(_run()) diff --git a/tests/unit/train/test_model_forward.py b/tests/unit/train/test_model_forward.py new file mode 100644 index 00000000000..b62805bf3d4 --- /dev/null +++ b/tests/unit/train/test_model_forward.py @@ -0,0 +1,50 @@ +from types import SimpleNamespace + +import torch +import torch.nn as nn + +from prime_rl.trainer.model import forward + + +class _CaptureModel(nn.Module): + def __init__(self, config: SimpleNamespace): + super().__init__() + self.config = config + self.kwargs = None + + def forward(self, **kwargs): + self.kwargs = kwargs + input_ids = kwargs["input_ids"] + return {"logits": torch.zeros(*input_ids.shape, 4)} + + +def test_forward_adds_qwen3_vl_mm_token_type_ids(): + image_token_id = 10 + video_token_id = 20 + model = _CaptureModel( + SimpleNamespace(model_type="qwen3_vl", image_token_id=image_token_id, video_token_id=video_token_id) + ) + input_ids = torch.tensor([[1, image_token_id, image_token_id, 2, video_token_id]]) + position_ids = torch.arange(input_ids.shape[1]).unsqueeze(0) + pixel_values = torch.ones(2, 3) + image_grid_thw = torch.tensor([[1, 1, 2]]) + + forward(model, input_ids, position_ids, pixel_values=pixel_values, image_grid_thw=image_grid_thw) + + assert model.kwargs is not None + assert "position_ids" not in model.kwargs + torch.testing.assert_close(model.kwargs["pixel_values"], pixel_values) + torch.testing.assert_close(model.kwargs["image_grid_thw"], image_grid_thw) + torch.testing.assert_close(model.kwargs["mm_token_type_ids"], torch.tensor([[0, 1, 1, 0, 2]])) + + +def test_forward_skips_mm_token_type_ids_for_other_vlm_models(): + model = _CaptureModel(SimpleNamespace(model_type="qwen3_5_moe", image_token_id=10)) + input_ids = torch.tensor([[1, 10, 10, 2]]) + position_ids = torch.arange(input_ids.shape[1]).unsqueeze(0) + + forward(model, input_ids, position_ids, pixel_values=torch.ones(2, 3), image_grid_thw=torch.tensor([[1, 1, 2]])) + + assert model.kwargs is not None + assert "position_ids" not in model.kwargs + assert "mm_token_type_ids" not in model.kwargs From b489632ea93d513cd9fa4543a0e79ab515c044dc Mon Sep 17 00:00:00 2001 From: hallerite Date: Wed, 8 Apr 2026 09:22:46 +0000 Subject: [PATCH 03/54] fix: restore serving chat type imports --- src/prime_rl/inference/vllm/serving_chat_with_tokens.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/prime_rl/inference/vllm/serving_chat_with_tokens.py b/src/prime_rl/inference/vllm/serving_chat_with_tokens.py index 78f06aa7461..2629dc27d47 100644 --- a/src/prime_rl/inference/vllm/serving_chat_with_tokens.py +++ b/src/prime_rl/inference/vllm/serving_chat_with_tokens.py @@ -4,9 +4,10 @@ import numpy as np from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest, ChatCompletionResponse from vllm.entrypoints.openai.chat_completion.serving import OpenAIServingChat -from vllm.entrypoints.openai.engine.protocol import ErrorResponse +from vllm.entrypoints.openai.engine.protocol import ErrorResponse, RequestResponseMetadata from vllm.logger import init_logger from vllm.outputs import RequestOutput +from vllm.reasoning import ReasoningParser logger = init_logger(__name__) From f34982aed0643731ba57e5abf90663c64f94b0c3 Mon Sep 17 00:00:00 2001 From: hallerite Date: Wed, 8 Apr 2026 09:24:43 +0000 Subject: [PATCH 04/54] feat: forward lora adapters through generate --- src/prime_rl/inference/vllm/serving_generate.py | 5 +++++ tests/unit/inference/test_serving_generate.py | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/prime_rl/inference/vllm/serving_generate.py b/src/prime_rl/inference/vllm/serving_generate.py index 83c86ee0f45..e9581fb4cb1 100644 --- a/src/prime_rl/inference/vllm/serving_generate.py +++ b/src/prime_rl/inference/vllm/serving_generate.py @@ -54,6 +54,7 @@ class GenerateRequest(BaseModel): repetition_penalty: float = 1.0 min_tokens: int = 0 prompt_logprobs: bool = False + priority: int = 0 class GenerateChoiceResponse(BaseModel): @@ -112,16 +113,20 @@ async def generate(self, request: GenerateRequest, raw_request: Request) -> Gene routed_experts_map: dict[int, dict] = {} final_output: RequestOutput | None = None data_parallel_rank = None + lora_request = None trace_headers = None if self.chat_handler is not None: data_parallel_rank = self.chat_handler._get_data_parallel_rank(raw_request) trace_headers = await self.chat_handler._get_trace_headers(raw_request.headers) + lora_request = self.chat_handler._maybe_get_adapters(request) generator = self.engine_client.generate( engine_prompt, sampling_params, request_id, + lora_request=lora_request, trace_headers=trace_headers, + priority=request.priority, data_parallel_rank=data_parallel_rank, ) diff --git a/tests/unit/inference/test_serving_generate.py b/tests/unit/inference/test_serving_generate.py index 508682b6bdf..f53641186d5 100644 --- a/tests/unit/inference/test_serving_generate.py +++ b/tests/unit/inference/test_serving_generate.py @@ -7,6 +7,9 @@ class _FakeChatHandler: + def __init__(self): + self.lora_request = object() + def _get_data_parallel_rank(self, raw_request): assert raw_request.headers["X-data-parallel-rank"] == "3" return 3 @@ -15,6 +18,10 @@ async def _get_trace_headers(self, headers): assert headers["traceparent"] == "trace" return {"traceparent": headers["traceparent"]} + def _maybe_get_adapters(self, request): + assert request.model == "test-model" + return self.lora_request + class _FakeEngineClient: def __init__(self): @@ -65,6 +72,7 @@ async def _run(): prompt_token_ids=[10, 11, 12], max_tokens=1, prompt_logprobs=True, + priority=7, ) response = await handler.generate(request, _FakeRawRequest()) @@ -77,6 +85,8 @@ async def _run(): call = engine_client.calls[0] assert call["engine_prompt"] == {"prompt_token_ids": [10, 11, 12]} assert call["sampling_params"].prompt_logprobs == 1 + assert call["kwargs"]["lora_request"] is handler.chat_handler.lora_request + assert call["kwargs"]["priority"] == 7 assert call["kwargs"]["data_parallel_rank"] == 3 assert call["kwargs"]["trace_headers"] == {"traceparent": "trace"} From 600348b697ba721b30d961df2446222a6b11bd13 Mon Sep 17 00:00:00 2001 From: hallerite Date: Wed, 8 Apr 2026 17:28:54 +0000 Subject: [PATCH 05/54] fix: preserve renderer proxy client routing --- src/prime_rl/orchestrator/orchestrator.py | 30 ++--- src/prime_rl/orchestrator/scheduler.py | 13 ++- src/prime_rl/utils/client.py | 54 +++++++++ tests/unit/orchestrator/test_scheduler.py | 22 ++++ tests/unit/utils/test_client.py | 127 +++++++++++----------- 5 files changed, 165 insertions(+), 81 deletions(-) diff --git a/src/prime_rl/orchestrator/orchestrator.py b/src/prime_rl/orchestrator/orchestrator.py index a0a0245f545..956bc06ac14 100644 --- a/src/prime_rl/orchestrator/orchestrator.py +++ b/src/prime_rl/orchestrator/orchestrator.py @@ -60,10 +60,8 @@ wait_for_env_servers, ) from prime_rl.utils.client import ( - StaticInferencePool, + ProxiedInferencePool, init_nccl_broadcast, - setup_admin_clients, - setup_clients, setup_inference_pool, ) from prime_rl.utils.config import cli @@ -154,23 +152,27 @@ async def orchestrate(config: OrchestratorConfig): import uvicorn from renderers.proxy import RenderingProxy - proxy = RenderingProxy(renderer, vllm_base_url=rollout_client_config.base_url[0], processor=processor) + proxy = RenderingProxy( + renderer, + vllm_base_url=rollout_client_config.base_url[0] if rollout_client_config.base_url else None, + processor=processor, + ) proxy_port = 18100 proxy_server = uvicorn.Server(uvicorn.Config(proxy.app, host="127.0.0.1", port=proxy_port, log_level="warning")) proxy_server_task = asyncio.create_task(proxy_server.serve()) - logger.info(f"Started rendering proxy on port {proxy_port} → {rollout_client_config.base_url[0]}") - - from copy import deepcopy - - proxy_client_config = deepcopy(rollout_client_config) - proxy_client_config.base_url = [f"http://127.0.0.1:{proxy_port}/v1"] + logger.info(f"Started rendering proxy on port {proxy_port}") # Rollout clients go through proxy (standard chat format), admin clients talk to vLLM directly client_type = "openai_chat_completions" - inference_pool = StaticInferencePool( - clients=setup_clients(proxy_client_config, client_type=client_type), - admin_clients=setup_admin_clients(rollout_client_config), - skip_model_check=rollout_client_config.skip_model_check, + upstream_inference_pool = await setup_inference_pool( + rollout_client_config, + model_name=rollout_model_name, + client_type=client_type, + ) + inference_pool = ProxiedInferencePool( + upstream_inference_pool=upstream_inference_pool, + proxy_base_url=f"http://127.0.0.1:{proxy_port}/v1", + client_type=client_type, ) # Setup monitor diff --git a/src/prime_rl/orchestrator/scheduler.py b/src/prime_rl/orchestrator/scheduler.py index 1ee154d935c..831d2b7c805 100644 --- a/src/prime_rl/orchestrator/scheduler.py +++ b/src/prime_rl/orchestrator/scheduler.py @@ -14,7 +14,10 @@ from prime_rl.orchestrator.utils import get_sampling_args from prime_rl.orchestrator.vf_utils import get_seq_len, run_rollout from prime_rl.utils.async_utils import safe_cancel, safe_cancel_all -from prime_rl.utils.client import InferencePool +from prime_rl.utils.client import ( + RENDERER_UPSTREAM_BASE_URL_HEADER, + InferencePool, +) from prime_rl.utils.logger import ProgressTracker, get_logger from prime_rl.utils.temp_scheduling import compute_temperature from prime_rl.utils.utils import ( @@ -157,8 +160,12 @@ async def cancel_inflight_rollouts(self): self.cancelled_rollouts_count += count @staticmethod - def _client_identity(c: vf.ClientConfig) -> tuple[str, str | None]: - return (c.api_base_url, c.extra_headers.get("X-data-parallel-rank")) + def _client_identity(c: vf.ClientConfig) -> tuple[str, str | None, str | None]: + return ( + c.api_base_url, + c.extra_headers.get("X-data-parallel-rank"), + c.extra_headers.get(RENDERER_UPSTREAM_BASE_URL_HEADER), + ) async def _select_least_loaded_client(self) -> vf.ClientConfig: """Select the client with the fewest in-flight tasks. diff --git a/src/prime_rl/utils/client.py b/src/prime_rl/utils/client.py index d2e9cd9ae56..03b9af036e3 100644 --- a/src/prime_rl/utils/client.py +++ b/src/prime_rl/utils/client.py @@ -15,6 +15,8 @@ from prime_rl.configs.shared import ClientConfig from prime_rl.utils.logger import get_logger +RENDERER_UPSTREAM_BASE_URL_HEADER = "X-Renderer-Upstream-Base-URL" + @runtime_checkable class InferencePool(Protocol): @@ -96,6 +98,58 @@ async def stop(self) -> None: pass +class ProxiedInferencePool: + """Wrap an inference pool so requests flow through the local rendering proxy.""" + + def __init__( + self, + upstream_inference_pool: InferencePool, + proxy_base_url: str, + client_type: str = "openai_chat_completions", + ): + self._upstream_pool = upstream_inference_pool + self._proxy_base_url = proxy_base_url + self._client_type = client_type + + @property + def clients(self) -> list[vf.ClientConfig]: + return [self._to_proxy_client(client) for client in self._upstream_pool.clients] + + @property + def admin_clients(self) -> list[AsyncClient]: + return self._upstream_pool.admin_clients + + def update_model_name(self, model_name: str) -> None: + self._upstream_pool.update_model_name(model_name) + + async def get_next_client(self) -> vf.ClientConfig: + return self._to_proxy_client(await self._upstream_pool.get_next_client()) + + async def wait_for_ready(self, model_name: str, timeout: int = 1800) -> None: + await self._upstream_pool.wait_for_ready(model_name, timeout=timeout) + + async def update_weights(self, weight_dir: Path | None, lora_name: str | None = None, step: int = 0) -> None: + await self._upstream_pool.update_weights(weight_dir, lora_name=lora_name, step=step) + + def get_metrics(self) -> dict[str, float]: + return self._upstream_pool.get_metrics() + + async def stop(self) -> None: + await self._upstream_pool.stop() + + def _to_proxy_client(self, client: vf.ClientConfig) -> vf.ClientConfig: + headers = dict(client.extra_headers) + headers[RENDERER_UPSTREAM_BASE_URL_HEADER] = client.api_base_url + return client.model_copy( + update={ + "client_type": self._client_type, + "api_base_url": self._proxy_base_url, + "extra_headers": headers, + }, + deep=True, + ) + + async def setup_inference_pool( client_config: ClientConfig, model_name: str, client_type: str = "openai_chat_completions" ) -> InferencePool: diff --git a/tests/unit/orchestrator/test_scheduler.py b/tests/unit/orchestrator/test_scheduler.py index 46a999d487b..f537d5189ff 100644 --- a/tests/unit/orchestrator/test_scheduler.py +++ b/tests/unit/orchestrator/test_scheduler.py @@ -3,8 +3,11 @@ from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch +import verifiers as vf + from prime_rl.orchestrator.scheduler import InflightRolloutInfo, Scheduler from prime_rl.utils.async_utils import safe_cancel +from prime_rl.utils.client import RENDERER_UPSTREAM_BASE_URL_HEADER def make_scheduler() -> Scheduler: @@ -159,3 +162,22 @@ async def update_weights(weight_dir, lora_name=None, step=0) -> None: assert scheduler.inflight_policy_update_task is None asyncio.run(run()) + + +def test_client_identity_distinguishes_renderer_upstreams(): + client_a = vf.ClientConfig( + api_base_url="http://127.0.0.1:18100/v1", + extra_headers={ + "X-data-parallel-rank": "0", + RENDERER_UPSTREAM_BASE_URL_HEADER: "http://worker-a:8000/v1", + }, + ) + client_b = vf.ClientConfig( + api_base_url="http://127.0.0.1:18100/v1", + extra_headers={ + "X-data-parallel-rank": "0", + RENDERER_UPSTREAM_BASE_URL_HEADER: "http://worker-b:8000/v1", + }, + ) + + assert Scheduler._client_identity(client_a) != Scheduler._client_identity(client_b) diff --git a/tests/unit/utils/test_client.py b/tests/unit/utils/test_client.py index e107fb163f4..253b3987e5b 100644 --- a/tests/unit/utils/test_client.py +++ b/tests/unit/utils/test_client.py @@ -1,85 +1,84 @@ -import asyncio -from pathlib import Path -from unittest.mock import AsyncMock, MagicMock +from types import SimpleNamespace -import httpx -import pytest +import verifiers as vf -from prime_rl.utils.client import _is_retryable_lora_error, load_lora_adapter +from prime_rl.utils.client import ( + RENDERER_UPSTREAM_BASE_URL_HEADER, + ProxiedInferencePool, +) -def test_is_retryable_lora_error_returns_true_for_404(): - response = MagicMock() - response.status_code = 404 - error = httpx.HTTPStatusError("Not found", request=MagicMock(), response=response) - assert _is_retryable_lora_error(error) is True +class _FakeInferencePool: + def __init__(self, clients: list[vf.ClientConfig]): + self._clients = clients + self.admin_clients = [SimpleNamespace(base_url="http://admin")] + self.wait_for_ready_calls: list[tuple[str, int]] = [] + self.update_weights_calls: list[tuple[object, str | None, int]] = [] + self.stop_calls = 0 + self.updated_model_name = None + @property + def clients(self) -> list[vf.ClientConfig]: + return self._clients -def test_is_retryable_lora_error_returns_true_for_500(): - response = MagicMock() - response.status_code = 500 - error = httpx.HTTPStatusError("Server error", request=MagicMock(), response=response) - assert _is_retryable_lora_error(error) is True + def update_model_name(self, model_name: str) -> None: + self.updated_model_name = model_name + async def get_next_client(self) -> vf.ClientConfig: + return self._clients[0] -def test_is_retryable_lora_error_returns_false_for_400(): - response = MagicMock() - response.status_code = 400 - error = httpx.HTTPStatusError("Bad request", request=MagicMock(), response=response) - assert _is_retryable_lora_error(error) is False + async def wait_for_ready(self, model_name: str, timeout: int = 1800) -> None: + self.wait_for_ready_calls.append((model_name, timeout)) + async def update_weights(self, weight_dir, lora_name: str | None = None, step: int = 0) -> None: + self.update_weights_calls.append((weight_dir, lora_name, step)) -def test_is_retryable_lora_error_returns_false_for_non_http_error(): - assert _is_retryable_lora_error(ValueError("some error")) is False + def get_metrics(self) -> dict[str, float]: + return {"ready_servers": 2.0} + async def stop(self) -> None: + self.stop_calls += 1 -def test_load_lora_adapter_succeeds_on_first_attempt(): - mock_client = AsyncMock() - mock_response = MagicMock() - mock_response.raise_for_status = MagicMock() - mock_client.post.return_value = mock_response - asyncio.run(load_lora_adapter([mock_client], "test-lora", Path("/test/path"))) - - mock_client.post.assert_called_once_with( - "/load_lora_adapter", - json={"lora_name": "test-lora", "lora_path": "/test/path"}, +def _make_client(base_url: str, *, client_idx: int = 0, dp_rank: str | None = None) -> vf.ClientConfig: + headers = {} + if dp_rank is not None: + headers["X-data-parallel-rank"] = dp_rank + return vf.ClientConfig( + client_idx=client_idx, + client_type="openai_chat_completions", + api_key_var="PRIME_API_KEY", + api_base_url=base_url, + extra_headers=headers, + extra_headers_from_state={"X-Session-ID": "session_id"}, ) -def test_load_lora_adapter_retries_on_404_then_succeeds(): - mock_client = AsyncMock() - - error_response = MagicMock() - error_response.status_code = 404 - success_response = MagicMock() - success_response.raise_for_status = MagicMock() - - call_count = 0 - - async def mock_post(*args, **kwargs): - nonlocal call_count - call_count += 1 - if call_count == 1: - raise httpx.HTTPStatusError("Not found", request=MagicMock(), response=error_response) - return success_response - - mock_client.post = mock_post - - asyncio.run(load_lora_adapter([mock_client], "test-lora", Path("/test/path"))) - - assert call_count == 2 +def test_proxied_inference_pool_wraps_clients_with_upstream_header(): + upstream_pool = _FakeInferencePool( + [ + _make_client("http://worker-a:8000/v1", client_idx=0, dp_rank="0"), + _make_client("http://worker-b:8000/v1", client_idx=1, dp_rank="0"), + ] + ) + proxy_pool = ProxiedInferencePool(upstream_pool, proxy_base_url="http://127.0.0.1:18100/v1") + clients = proxy_pool.clients -def test_load_lora_adapter_raises_non_retryable_error_immediately(): - mock_client = AsyncMock() + assert [client.api_base_url for client in clients] == ["http://127.0.0.1:18100/v1"] * 2 + assert [client.extra_headers[RENDERER_UPSTREAM_BASE_URL_HEADER] for client in clients] == [ + "http://worker-a:8000/v1", + "http://worker-b:8000/v1", + ] + assert clients[0].extra_headers["X-data-parallel-rank"] == "0" + assert clients[0].extra_headers_from_state == {"X-Session-ID": "session_id"} + assert upstream_pool.clients[0].api_base_url == "http://worker-a:8000/v1" + assert RENDERER_UPSTREAM_BASE_URL_HEADER not in upstream_pool.clients[0].extra_headers - error_response = MagicMock() - error_response.status_code = 400 - mock_client.post.side_effect = httpx.HTTPStatusError("Bad request", request=MagicMock(), response=error_response) - with pytest.raises(httpx.HTTPStatusError) as exc_info: - asyncio.run(load_lora_adapter([mock_client], "test-lora", Path("/test/path"))) +def test_proxied_inference_pool_delegates_admin_and_metrics(): + upstream_pool = _FakeInferencePool([_make_client("http://worker-a:8000/v1", client_idx=0)]) + proxy_pool = ProxiedInferencePool(upstream_pool, proxy_base_url="http://127.0.0.1:18100/v1") - assert exc_info.value.response.status_code == 400 - assert mock_client.post.call_count == 1 + assert proxy_pool.admin_clients == upstream_pool.admin_clients + assert proxy_pool.get_metrics() == {"ready_servers": 2.0} From 82e929b5874670b4061afefbfc8910c41f4a4a42 Mon Sep 17 00:00:00 2001 From: hallerite Date: Wed, 8 Apr 2026 17:48:03 +0000 Subject: [PATCH 06/54] fix: restore external rollout model routing --- configs/debug/rl_lora_smoke.toml | 42 +++++++ .../teacher_rollout_hard_distill_smoke.toml | 37 ++++++ skills/entrypoints/SKILL.md | 4 + src/prime_rl/orchestrator/orchestrator.py | 103 ++++++++++------ .../orchestrator/test_orchestrator_setup.py | 113 ++++++++++++++++++ 5 files changed, 260 insertions(+), 39 deletions(-) create mode 100644 configs/debug/rl_lora_smoke.toml create mode 100644 configs/debug/teacher_rollout_hard_distill_smoke.toml create mode 100644 tests/unit/orchestrator/test_orchestrator_setup.py diff --git a/configs/debug/rl_lora_smoke.toml b/configs/debug/rl_lora_smoke.toml new file mode 100644 index 00000000000..dd47ab0cf8f --- /dev/null +++ b/configs/debug/rl_lora_smoke.toml @@ -0,0 +1,42 @@ +max_steps = 4 +max_async_level = 0 +seq_len = 1024 +output_dir = "outputs/rl_lora_smoke" + +[ckpt] +interval = 1 + +[deployment] +type = "single_node" +gpus_per_node = 2 +num_train_gpus = 1 +num_infer_gpus = 1 + +[model] +name = "PrimeIntellect/Qwen3-0.6B-Reverse-Text-SFT" + +[trainer.optim] +lr = 5e-5 + +[trainer.model.lora] +rank = 8 + +[trainer.ckpt.weights] +save_adapter_separately = true + +[orchestrator] +batch_size = 16 +rollouts_per_example = 4 + +[orchestrator.model.lora] +name = "r8-smoke" + +[orchestrator.sampling] +max_tokens = 128 + +[[orchestrator.env]] +id = "reverse-text" + +[inference] +enable_lora = true +gpu_memory_utilization = 0.7 diff --git a/configs/debug/teacher_rollout_hard_distill_smoke.toml b/configs/debug/teacher_rollout_hard_distill_smoke.toml new file mode 100644 index 00000000000..d83b3fe605d --- /dev/null +++ b/configs/debug/teacher_rollout_hard_distill_smoke.toml @@ -0,0 +1,37 @@ +max_steps = 1 +seq_len = 1024 +output_dir = "outputs/teacher_rollout_hard_distill_smoke" + +[deployment] +type = "single_node" +gpus_per_node = 2 +num_train_gpus = 1 +num_infer_gpus = 0 + +[model] +name = "PrimeIntellect/Qwen3-0.6B-Reverse-Text-SFT" + +[trainer.loss] +type = "sft" + +[trainer.optim] +lr = 1e-5 + +[orchestrator] +batch_size = 8 +rollouts_per_example = 2 + +[orchestrator.sampling] +max_tokens = 64 +temperature = 0.7 + +[orchestrator.teacher_rollout_model.client] +base_url = ["https://api.pinference.ai/api/v1"] +api_key_var = "PRIME_API_KEY" +skip_model_check = true + +[orchestrator.teacher_rollout_model.model] +name = "PrimeIntellect/Qwen3-0.6B-Reverse-Text-SFT" + +[[orchestrator.env]] +id = "reverse-text" diff --git a/skills/entrypoints/SKILL.md b/skills/entrypoints/SKILL.md index 2e00ec28ccd..09ac3bd7288 100644 --- a/skills/entrypoints/SKILL.md +++ b/skills/entrypoints/SKILL.md @@ -25,6 +25,10 @@ uv run --extra flash-attn --extra envs rl @ configs/multimodal/rl_color_codeword Short debug runs can write large filesystem weight-broadcast snapshots. If `/` is low on space, put the debug `--output-dir` on `/dev/shm` and clean it up afterward. +For hard-distillation with `orchestrator.teacher_rollout_model`, omit `[inference]`. That mode talks directly to the external OpenAI-compatible rollout endpoint and reconstructs training tokens from messages; it does not use the local renderer proxy or `/v1/generate`. + +For LoRA RL smoke runs against local vLLM, set `VLLM_ALLOW_RUNTIME_LORA_UPDATING=True` in the environment or adapter load/update requests will fail. + For ad-hoc `CliAgentEnv`/`ComposableEnv` sandbox smoke tests that call back through Prime Tunnel using Python stdlib `urllib`, set an explicit non-default `User-Agent` header such as `curl/7.81.0`. The tunnel can reject Python's default `urllib` user agent from inside Prime sandboxes with HTTP 403 / code 1010. Prefer uploading an agent script into the sandbox and running `python3 /tmp/agent.py` over passing a heredoc directly as a `start_background_job` command. - **Config:** `RLConfig` (`src/prime_rl/configs/rl.py`) diff --git a/src/prime_rl/orchestrator/orchestrator.py b/src/prime_rl/orchestrator/orchestrator.py index 956bc06ac14..76ced701263 100644 --- a/src/prime_rl/orchestrator/orchestrator.py +++ b/src/prime_rl/orchestrator/orchestrator.py @@ -31,7 +31,10 @@ # Import environment before any other imports import pandas as pd +import uvicorn import verifiers as vf +from renderers.base import create_renderer +from renderers.proxy import RenderingProxy from transformers import AutoProcessor, AutoTokenizer from prime_rl.configs.orchestrator import BufferConfig, OrchestratorConfig @@ -141,38 +144,13 @@ async def orchestrate(config: OrchestratorConfig): config.model.name, trust_remote_code=config.model.trust_remote_code, use_fast=True ) - # Create renderer and start rendering proxy. - # The proxy sits between verifiers and vLLM: verifiers sends standard chat - # messages → proxy renders to tokens via Renderer → forwards to vLLM /v1/generate. - from renderers.base import create_renderer as _create_renderer - - renderer = _create_renderer(tokenizer, renderer=config.model.renderer) - logger.info(f"Initialized {type(renderer).__name__} for {config.model.name}") - - import uvicorn - from renderers.proxy import RenderingProxy - - proxy = RenderingProxy( - renderer, - vllm_base_url=rollout_client_config.base_url[0] if rollout_client_config.base_url else None, + renderer, inference_pool, proxy, proxy_server, proxy_server_task = await setup_rollout_inference_pool( + config=config, + rollout_client_config=rollout_client_config, + rollout_model_name=rollout_model_name, + tokenizer=tokenizer, processor=processor, - ) - proxy_port = 18100 - proxy_server = uvicorn.Server(uvicorn.Config(proxy.app, host="127.0.0.1", port=proxy_port, log_level="warning")) - proxy_server_task = asyncio.create_task(proxy_server.serve()) - logger.info(f"Started rendering proxy on port {proxy_port}") - - # Rollout clients go through proxy (standard chat format), admin clients talk to vLLM directly - client_type = "openai_chat_completions" - upstream_inference_pool = await setup_inference_pool( - rollout_client_config, - model_name=rollout_model_name, - client_type=client_type, - ) - inference_pool = ProxiedInferencePool( - upstream_inference_pool=upstream_inference_pool, - proxy_base_url=f"http://127.0.0.1:{proxy_port}/v1", - client_type=client_type, + logger=logger, ) # Setup monitor @@ -929,14 +907,15 @@ def compute_solve_rates(df): # Stop inference pool await inference_pool.stop() - proxy_server.should_exit = True - try: - await asyncio.wait_for(proxy_server_task, timeout=5) - except TimeoutError: - proxy_server_task.cancel() - with suppress(asyncio.CancelledError): - await proxy_server_task - await proxy.close() + if proxy_server is not None and proxy_server_task is not None and proxy is not None: + proxy_server.should_exit = True + try: + await asyncio.wait_for(proxy_server_task, timeout=5) + except TimeoutError: + proxy_server_task.cancel() + with suppress(asyncio.CancelledError): + await proxy_server_task + await proxy.close() if teacher_inference_pool is not None: await teacher_inference_pool.stop() @@ -961,5 +940,51 @@ def main(): asyncio.run(orchestrate(cli(OrchestratorConfig))) +async def setup_rollout_inference_pool( + *, + config: OrchestratorConfig, + rollout_client_config, + rollout_model_name: str, + tokenizer, + processor, + logger, +): + """Set up rollout inference for either local renderer/vLLM or external hard distillation.""" + client_type = "openai_chat_completions" + if config.teacher_rollout_model is not None: + logger.info("Using external rollout model without renderer proxy") + inference_pool = await setup_inference_pool( + rollout_client_config, + model_name=rollout_model_name, + client_type=client_type, + ) + return None, inference_pool, None, None, None + + renderer = create_renderer(tokenizer, renderer=config.model.renderer) + logger.info(f"Initialized {type(renderer).__name__} for {config.model.name}") + + proxy = RenderingProxy( + renderer, + vllm_base_url=rollout_client_config.base_url[0] if rollout_client_config.base_url else None, + processor=processor, + ) + proxy_port = 18100 + proxy_server = uvicorn.Server(uvicorn.Config(proxy.app, host="127.0.0.1", port=proxy_port, log_level="warning")) + proxy_server_task = asyncio.create_task(proxy_server.serve()) + logger.info(f"Started rendering proxy on port {proxy_port}") + + upstream_inference_pool = await setup_inference_pool( + rollout_client_config, + model_name=rollout_model_name, + client_type=client_type, + ) + inference_pool = ProxiedInferencePool( + upstream_inference_pool=upstream_inference_pool, + proxy_base_url=f"http://127.0.0.1:{proxy_port}/v1", + client_type=client_type, + ) + return renderer, inference_pool, proxy, proxy_server, proxy_server_task + + if __name__ == "__main__": main() diff --git a/tests/unit/orchestrator/test_orchestrator_setup.py b/tests/unit/orchestrator/test_orchestrator_setup.py new file mode 100644 index 00000000000..2d4ce0f7191 --- /dev/null +++ b/tests/unit/orchestrator/test_orchestrator_setup.py @@ -0,0 +1,113 @@ +import asyncio +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock, patch + +from prime_rl.orchestrator.orchestrator import setup_rollout_inference_pool + + +def test_setup_rollout_inference_pool_uses_plain_client_for_external_teacher_rollout(): + async def run() -> None: + config = SimpleNamespace( + teacher_rollout_model=SimpleNamespace(), + model=SimpleNamespace(renderer="auto", name="student-model"), + ) + rollout_client_config = SimpleNamespace(base_url=["https://api.pinference.ai/api/v1"]) + logger = MagicMock() + inference_pool = object() + + with ( + patch( + "prime_rl.orchestrator.orchestrator.setup_inference_pool", new=AsyncMock(return_value=inference_pool) + ), + patch("prime_rl.orchestrator.orchestrator.create_renderer") as create_renderer_mock, + patch("prime_rl.orchestrator.orchestrator.RenderingProxy") as proxy_mock, + patch("prime_rl.orchestrator.orchestrator.ProxiedInferencePool") as proxied_pool_mock, + patch("prime_rl.orchestrator.orchestrator.asyncio.create_task") as create_task_mock, + ): + renderer, returned_pool, proxy, proxy_server, proxy_task = await setup_rollout_inference_pool( + config=config, + rollout_client_config=rollout_client_config, + rollout_model_name="teacher-model", + tokenizer=object(), + processor=None, + logger=logger, + ) + + assert renderer is None + assert returned_pool is inference_pool + assert proxy is None + assert proxy_server is None + assert proxy_task is None + create_renderer_mock.assert_not_called() + proxy_mock.assert_not_called() + proxied_pool_mock.assert_not_called() + create_task_mock.assert_not_called() + + asyncio.run(run()) + + +def test_setup_rollout_inference_pool_wraps_local_vllm_with_renderer_proxy(): + async def run() -> None: + config = SimpleNamespace( + teacher_rollout_model=None, + model=SimpleNamespace(renderer="auto", name="student-model"), + ) + rollout_client_config = SimpleNamespace(base_url=["http://localhost:8000/v1"]) + logger = MagicMock() + renderer = object() + upstream_pool = object() + proxy = SimpleNamespace(app=object()) + proxy_server = SimpleNamespace(serve=MagicMock(return_value=object())) + proxy_task = object() + inference_pool = object() + + with ( + patch("prime_rl.orchestrator.orchestrator.create_renderer", return_value=renderer) as create_renderer_mock, + patch("prime_rl.orchestrator.orchestrator.RenderingProxy", return_value=proxy) as proxy_cls_mock, + patch("prime_rl.orchestrator.orchestrator.uvicorn.Config", return_value=object()) as uvicorn_config_mock, + patch( + "prime_rl.orchestrator.orchestrator.uvicorn.Server", return_value=proxy_server + ) as uvicorn_server_mock, + patch( + "prime_rl.orchestrator.orchestrator.setup_inference_pool", + new=AsyncMock(return_value=upstream_pool), + ) as setup_pool_mock, + patch( + "prime_rl.orchestrator.orchestrator.ProxiedInferencePool", + return_value=inference_pool, + ) as proxied_pool_mock, + patch( + "prime_rl.orchestrator.orchestrator.asyncio.create_task", return_value=proxy_task + ) as create_task_mock, + ): + ( + returned_renderer, + returned_pool, + returned_proxy, + returned_server, + returned_task, + ) = await setup_rollout_inference_pool( + config=config, + rollout_client_config=rollout_client_config, + rollout_model_name="student-model", + tokenizer=object(), + processor="processor", + logger=logger, + ) + + assert returned_renderer is renderer + assert returned_pool is inference_pool + assert returned_proxy is proxy + assert returned_server is proxy_server + assert returned_task is proxy_task + create_renderer_mock.assert_called_once() + proxy_cls_mock.assert_called_once_with( + renderer, vllm_base_url="http://localhost:8000/v1", processor="processor" + ) + uvicorn_config_mock.assert_called_once() + uvicorn_server_mock.assert_called_once() + setup_pool_mock.assert_awaited_once() + proxied_pool_mock.assert_called_once() + create_task_mock.assert_called_once() + + asyncio.run(run()) From 82fcbeb09136ac88e1e81595f8369ae650bb57ea Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 9 Apr 2026 15:22:07 +0000 Subject: [PATCH 07/54] refactor: use direct renderer rollout clients --- skills/entrypoints/SKILL.md | 2 +- src/prime_rl/inference/vllm/server.py | 10 +- ...py => serving_chat_with_routed_experts.py} | 10 +- src/prime_rl/orchestrator/orchestrator.py | 51 ++------ src/prime_rl/orchestrator/scheduler.py | 8 +- src/prime_rl/utils/client.py | 82 ++++-------- src/prime_rl/utils/elastic.py | 12 +- .../orchestrator/test_orchestrator_setup.py | 66 +++------- tests/unit/orchestrator/test_scheduler.py | 17 +-- tests/unit/utils/test_client.py | 123 ++++++++---------- tests/unit/utils/test_elastic.py | 44 +++++++ uv.lock | 8 +- 12 files changed, 175 insertions(+), 258 deletions(-) rename src/prime_rl/inference/vllm/{serving_chat_with_tokens.py => serving_chat_with_routed_experts.py} (76%) diff --git a/skills/entrypoints/SKILL.md b/skills/entrypoints/SKILL.md index 09ac3bd7288..bf0888b7b30 100644 --- a/skills/entrypoints/SKILL.md +++ b/skills/entrypoints/SKILL.md @@ -25,7 +25,7 @@ uv run --extra flash-attn --extra envs rl @ configs/multimodal/rl_color_codeword Short debug runs can write large filesystem weight-broadcast snapshots. If `/` is low on space, put the debug `--output-dir` on `/dev/shm` and clean it up afterward. -For hard-distillation with `orchestrator.teacher_rollout_model`, omit `[inference]`. That mode talks directly to the external OpenAI-compatible rollout endpoint and reconstructs training tokens from messages; it does not use the local renderer proxy or `/v1/generate`. +For hard-distillation with `orchestrator.teacher_rollout_model`, omit `[inference]`. That mode talks directly to the external OpenAI-compatible rollout endpoint and reconstructs training tokens from messages; it does not use the local direct renderer client or `/v1/generate`. For LoRA RL smoke runs against local vLLM, set `VLLM_ALLOW_RUNTIME_LORA_UPDATING=True` in the environment or adapter load/update requests will fail. diff --git a/src/prime_rl/inference/vllm/server.py b/src/prime_rl/inference/vllm/server.py index 0c523e94623..c635dc8f931 100644 --- a/src/prime_rl/inference/vllm/server.py +++ b/src/prime_rl/inference/vllm/server.py @@ -133,8 +133,8 @@ def resolve_tool_call_parser(model_name: str, tool_call_parser: str | None) -> s monkey_patch_load_lora_adapter, monkey_patch_tokenize_params_validation, ) -from prime_rl.inference.vllm.serving_chat_with_tokens import ( - OpenAIServingChatWithTokens, +from prime_rl.inference.vllm.serving_chat_with_routed_experts import ( + OpenAIServingChatWithRoutedExperts, ) # NOTE: Fix harmony stop token propagation for GPT-OSS models @@ -254,14 +254,14 @@ async def custom_init_app_state( """ Modifies init_app_state: 1. Call the original init_app_state to set up standard state. - 2. Replace the serving_chat with our wrapper so chat responses can still - expose routed experts when requested. + 2. Replace the serving_chat with our routed-experts wrapper so chat responses + can still expose routed experts when requested. """ await init_app_state(engine_client, state, args, supported_tasks) if "generate" in supported_tasks and state.openai_serving_chat is not None: original_chat = state.openai_serving_chat - serving_chat = object.__new__(OpenAIServingChatWithTokens) + serving_chat = object.__new__(OpenAIServingChatWithRoutedExperts) serving_chat.__dict__.update(original_chat.__dict__) state.openai_serving_chat = serving_chat else: diff --git a/src/prime_rl/inference/vllm/serving_chat_with_tokens.py b/src/prime_rl/inference/vllm/serving_chat_with_routed_experts.py similarity index 76% rename from src/prime_rl/inference/vllm/serving_chat_with_tokens.py rename to src/prime_rl/inference/vllm/serving_chat_with_routed_experts.py index 2629dc27d47..d1ebefda387 100644 --- a/src/prime_rl/inference/vllm/serving_chat_with_tokens.py +++ b/src/prime_rl/inference/vllm/serving_chat_with_routed_experts.py @@ -36,8 +36,8 @@ def post_process(self, response: ChatCompletionResponse): choice.routed_experts = self.routed_experts[choice.index] -class OpenAIServingChatWithTokens(OpenAIServingChat): - """OpenAI-compatible generate API that allows token-in and routed experts capture.""" +class OpenAIServingChatWithRoutedExperts(OpenAIServingChat): + """OpenAI chat serving wrapper that preserves routed expert metadata.""" async def chat_completion_full_generator( self, @@ -50,12 +50,6 @@ async def chat_completion_full_generator( request_metadata: RequestResponseMetadata, reasoning_parser: ReasoningParser | None = None, ) -> ErrorResponse | ChatCompletionResponse: - # We need to override the full_generator to be able to capture the routed experts - # By default, VLLM does not save the routed experts into ChatCompletionResponse.choices, so we need to capture them manually - # How this works: - # 1. We create a custom generator that encapsulates the original result_generator in self._generator - # 2. We override it's __aiter__ method to also capture the routed experts as an extra field in ChatCompletionResponse.choices - # 3. We override the full_generator method to use the custom generator instead of the original one if expert routing is enabled if self.model_config.enable_return_routed_experts: capture = _RoutedExpertsCapture(result_generator) result_generator = capture diff --git a/src/prime_rl/orchestrator/orchestrator.py b/src/prime_rl/orchestrator/orchestrator.py index 76ced701263..f9c21b7b3f3 100644 --- a/src/prime_rl/orchestrator/orchestrator.py +++ b/src/prime_rl/orchestrator/orchestrator.py @@ -4,7 +4,6 @@ import multiprocessing as mp import time from concurrent.futures import ThreadPoolExecutor -from contextlib import suppress import tomli_w @@ -31,10 +30,8 @@ # Import environment before any other imports import pandas as pd -import uvicorn import verifiers as vf from renderers.base import create_renderer -from renderers.proxy import RenderingProxy from transformers import AutoProcessor, AutoTokenizer from prime_rl.configs.orchestrator import BufferConfig, OrchestratorConfig @@ -63,7 +60,6 @@ wait_for_env_servers, ) from prime_rl.utils.client import ( - ProxiedInferencePool, init_nccl_broadcast, setup_inference_pool, ) @@ -144,12 +140,11 @@ async def orchestrate(config: OrchestratorConfig): config.model.name, trust_remote_code=config.model.trust_remote_code, use_fast=True ) - renderer, inference_pool, proxy, proxy_server, proxy_server_task = await setup_rollout_inference_pool( + renderer, inference_pool = await setup_rollout_inference_pool( config=config, rollout_client_config=rollout_client_config, rollout_model_name=rollout_model_name, tokenizer=tokenizer, - processor=processor, logger=logger, ) @@ -907,16 +902,6 @@ def compute_solve_rates(df): # Stop inference pool await inference_pool.stop() - if proxy_server is not None and proxy_server_task is not None and proxy is not None: - proxy_server.should_exit = True - try: - await asyncio.wait_for(proxy_server_task, timeout=5) - except TimeoutError: - proxy_server_task.cancel() - with suppress(asyncio.CancelledError): - await proxy_server_task - await proxy.close() - if teacher_inference_pool is not None: await teacher_inference_pool.stop() @@ -946,44 +931,28 @@ async def setup_rollout_inference_pool( rollout_client_config, rollout_model_name: str, tokenizer, - processor, logger, ): - """Set up rollout inference for either local renderer/vLLM or external hard distillation.""" - client_type = "openai_chat_completions" + """Set up rollout inference for either direct renderer clients or external hard distillation.""" if config.teacher_rollout_model is not None: - logger.info("Using external rollout model without renderer proxy") + logger.info("Using external rollout model without renderer client") inference_pool = await setup_inference_pool( rollout_client_config, model_name=rollout_model_name, - client_type=client_type, + client_type="openai_chat_completions", ) - return None, inference_pool, None, None, None + return None, inference_pool renderer = create_renderer(tokenizer, renderer=config.model.renderer) logger.info(f"Initialized {type(renderer).__name__} for {config.model.name}") - - proxy = RenderingProxy( - renderer, - vllm_base_url=rollout_client_config.base_url[0] if rollout_client_config.base_url else None, - processor=processor, - ) - proxy_port = 18100 - proxy_server = uvicorn.Server(uvicorn.Config(proxy.app, host="127.0.0.1", port=proxy_port, log_level="warning")) - proxy_server_task = asyncio.create_task(proxy_server.serve()) - logger.info(f"Started rendering proxy on port {proxy_port}") - - upstream_inference_pool = await setup_inference_pool( + inference_pool = await setup_inference_pool( rollout_client_config, model_name=rollout_model_name, - client_type=client_type, - ) - inference_pool = ProxiedInferencePool( - upstream_inference_pool=upstream_inference_pool, - proxy_base_url=f"http://127.0.0.1:{proxy_port}/v1", - client_type=client_type, + client_type="renderer", + renderer_name=config.model.renderer, ) - return renderer, inference_pool, proxy, proxy_server, proxy_server_task + logger.info("Using direct renderer rollout client") + return renderer, inference_pool if __name__ == "__main__": diff --git a/src/prime_rl/orchestrator/scheduler.py b/src/prime_rl/orchestrator/scheduler.py index 831d2b7c805..53edf8c3b78 100644 --- a/src/prime_rl/orchestrator/scheduler.py +++ b/src/prime_rl/orchestrator/scheduler.py @@ -14,10 +14,7 @@ from prime_rl.orchestrator.utils import get_sampling_args from prime_rl.orchestrator.vf_utils import get_seq_len, run_rollout from prime_rl.utils.async_utils import safe_cancel, safe_cancel_all -from prime_rl.utils.client import ( - RENDERER_UPSTREAM_BASE_URL_HEADER, - InferencePool, -) +from prime_rl.utils.client import InferencePool from prime_rl.utils.logger import ProgressTracker, get_logger from prime_rl.utils.temp_scheduling import compute_temperature from prime_rl.utils.utils import ( @@ -160,11 +157,10 @@ async def cancel_inflight_rollouts(self): self.cancelled_rollouts_count += count @staticmethod - def _client_identity(c: vf.ClientConfig) -> tuple[str, str | None, str | None]: + def _client_identity(c: vf.ClientConfig) -> tuple[str, str | None]: return ( c.api_base_url, c.extra_headers.get("X-data-parallel-rank"), - c.extra_headers.get(RENDERER_UPSTREAM_BASE_URL_HEADER), ) async def _select_least_loaded_client(self) -> vf.ClientConfig: diff --git a/src/prime_rl/utils/client.py b/src/prime_rl/utils/client.py index 03b9af036e3..412d1dd319b 100644 --- a/src/prime_rl/utils/client.py +++ b/src/prime_rl/utils/client.py @@ -15,8 +15,6 @@ from prime_rl.configs.shared import ClientConfig from prime_rl.utils.logger import get_logger -RENDERER_UPSTREAM_BASE_URL_HEADER = "X-Renderer-Upstream-Base-URL" - @runtime_checkable class InferencePool(Protocol): @@ -98,60 +96,11 @@ async def stop(self) -> None: pass -class ProxiedInferencePool: - """Wrap an inference pool so requests flow through the local rendering proxy.""" - - def __init__( - self, - upstream_inference_pool: InferencePool, - proxy_base_url: str, - client_type: str = "openai_chat_completions", - ): - self._upstream_pool = upstream_inference_pool - self._proxy_base_url = proxy_base_url - self._client_type = client_type - - @property - def clients(self) -> list[vf.ClientConfig]: - return [self._to_proxy_client(client) for client in self._upstream_pool.clients] - - @property - def admin_clients(self) -> list[AsyncClient]: - return self._upstream_pool.admin_clients - - def update_model_name(self, model_name: str) -> None: - self._upstream_pool.update_model_name(model_name) - - async def get_next_client(self) -> vf.ClientConfig: - return self._to_proxy_client(await self._upstream_pool.get_next_client()) - - async def wait_for_ready(self, model_name: str, timeout: int = 1800) -> None: - await self._upstream_pool.wait_for_ready(model_name, timeout=timeout) - - async def update_weights(self, weight_dir: Path | None, lora_name: str | None = None, step: int = 0) -> None: - await self._upstream_pool.update_weights(weight_dir, lora_name=lora_name, step=step) - - def get_metrics(self) -> dict[str, float]: - return self._upstream_pool.get_metrics() - - async def stop(self) -> None: - await self._upstream_pool.stop() - - def _to_proxy_client(self, client: vf.ClientConfig) -> vf.ClientConfig: - headers = dict(client.extra_headers) - headers[RENDERER_UPSTREAM_BASE_URL_HEADER] = client.api_base_url - return client.model_copy( - update={ - "client_type": self._client_type, - "api_base_url": self._proxy_base_url, - "extra_headers": headers, - }, - deep=True, - ) - - async def setup_inference_pool( - client_config: ClientConfig, model_name: str, client_type: str = "openai_chat_completions" + client_config: ClientConfig, + model_name: str, + client_type: str = "openai_chat_completions", + renderer_name: str = "auto", ) -> InferencePool: """Create an inference pool from config (static or elastic).""" logger = get_logger() @@ -159,7 +108,12 @@ async def setup_inference_pool( if client_config.is_elastic: from prime_rl.utils.elastic import ElasticInferencePool - return await ElasticInferencePool.from_config(client_config, model_name=model_name, client_type=client_type) + return await ElasticInferencePool.from_config( + client_config, + model_name=model_name, + client_type=client_type, + renderer_name=renderer_name, + ) logger.info( f"Initializing static inference pool (base_url={', '.join(client_config.base_url)}, " @@ -167,13 +121,23 @@ async def setup_inference_pool( f"api_key_var={client_config.api_key_var}, headers={client_config.headers})" ) return StaticInferencePool( - clients=setup_clients(client_config, client_type=client_type), + clients=setup_clients( + client_config, + client_type=client_type, + renderer_name=renderer_name, + renderer_model_name=model_name if client_type == "renderer" else None, + ), admin_clients=setup_admin_clients(client_config), skip_model_check=client_config.skip_model_check, ) -def setup_clients(client_config: ClientConfig, client_type: str = "openai_chat_completions") -> list[vf.ClientConfig]: +def setup_clients( + client_config: ClientConfig, + client_type: str = "openai_chat_completions", + renderer_name: str = "auto", + renderer_model_name: str | None = None, +) -> list[vf.ClientConfig]: clients = [] client_idx = 0 for base_url in client_config.base_url: @@ -185,6 +149,8 @@ def setup_clients(client_config: ClientConfig, client_type: str = "openai_chat_c vf.ClientConfig( client_idx=client_idx, client_type=client_type, + renderer=renderer_name, + renderer_model_name=renderer_model_name, api_base_url=base_url, api_key_var=client_config.api_key_var, timeout=client_config.timeout, diff --git a/src/prime_rl/utils/elastic.py b/src/prime_rl/utils/elastic.py index f6f54138728..7cffa82d783 100644 --- a/src/prime_rl/utils/elastic.py +++ b/src/prime_rl/utils/elastic.py @@ -108,6 +108,7 @@ def __init__( port: int = 8000, sync_interval: float = 5.0, client_type: str = "openai_chat_completions", + renderer_name: str = "auto", router_url: str | None = None, ): self.logger = get_logger() @@ -115,9 +116,11 @@ def __init__( self.client_config = client_config self.model_name = model_name self.base_model_name = model_name # Keep original for health checks + self.renderer_model_name = model_name if client_type == "renderer" else None self.port = port self.sync_interval = sync_interval self.client_type = client_type + self.renderer_name = renderer_name self.router_url = router_url self._servers: dict[str, ServerState] = {} @@ -134,7 +137,11 @@ def __init__( @classmethod async def from_config( - cls, config: ClientConfig, model_name: str, client_type: str = "openai_chat_completions" + cls, + config: ClientConfig, + model_name: str, + client_type: str = "openai_chat_completions", + renderer_name: str = "auto", ) -> ElasticInferencePool: if config.elastic is None: raise ValueError("Elastic inference pool requires elastic config") @@ -145,6 +152,7 @@ async def from_config( port=config.elastic.port, sync_interval=config.elastic.sync_interval, client_type=client_type, + renderer_name=renderer_name, router_url=config.router_url, ) await pool.start() @@ -188,6 +196,8 @@ def clients(self) -> list[vf.ClientConfig]: dp_rank_count=self.client_config.dp_rank_count, ), client_type=self.client_type, + renderer_name=self.renderer_name, + renderer_model_name=self.renderer_model_name, ) if urls else [] diff --git a/tests/unit/orchestrator/test_orchestrator_setup.py b/tests/unit/orchestrator/test_orchestrator_setup.py index 2d4ce0f7191..a0f4d669f11 100644 --- a/tests/unit/orchestrator/test_orchestrator_setup.py +++ b/tests/unit/orchestrator/test_orchestrator_setup.py @@ -7,6 +7,7 @@ def test_setup_rollout_inference_pool_uses_plain_client_for_external_teacher_rollout(): async def run() -> None: + tokenizer = object() config = SimpleNamespace( teacher_rollout_model=SimpleNamespace(), model=SimpleNamespace(renderer="auto", name="student-model"), @@ -20,94 +21,57 @@ async def run() -> None: "prime_rl.orchestrator.orchestrator.setup_inference_pool", new=AsyncMock(return_value=inference_pool) ), patch("prime_rl.orchestrator.orchestrator.create_renderer") as create_renderer_mock, - patch("prime_rl.orchestrator.orchestrator.RenderingProxy") as proxy_mock, - patch("prime_rl.orchestrator.orchestrator.ProxiedInferencePool") as proxied_pool_mock, - patch("prime_rl.orchestrator.orchestrator.asyncio.create_task") as create_task_mock, ): - renderer, returned_pool, proxy, proxy_server, proxy_task = await setup_rollout_inference_pool( + renderer, returned_pool = await setup_rollout_inference_pool( config=config, rollout_client_config=rollout_client_config, rollout_model_name="teacher-model", - tokenizer=object(), - processor=None, + tokenizer=tokenizer, logger=logger, ) assert renderer is None assert returned_pool is inference_pool - assert proxy is None - assert proxy_server is None - assert proxy_task is None create_renderer_mock.assert_not_called() - proxy_mock.assert_not_called() - proxied_pool_mock.assert_not_called() - create_task_mock.assert_not_called() asyncio.run(run()) -def test_setup_rollout_inference_pool_wraps_local_vllm_with_renderer_proxy(): +def test_setup_rollout_inference_pool_uses_direct_renderer_client_for_local_vllm(): async def run() -> None: + tokenizer = object() config = SimpleNamespace( teacher_rollout_model=None, - model=SimpleNamespace(renderer="auto", name="student-model"), + model=SimpleNamespace(renderer="qwen3_vl", name="student-model"), ) rollout_client_config = SimpleNamespace(base_url=["http://localhost:8000/v1"]) logger = MagicMock() renderer = object() - upstream_pool = object() - proxy = SimpleNamespace(app=object()) - proxy_server = SimpleNamespace(serve=MagicMock(return_value=object())) - proxy_task = object() inference_pool = object() with ( patch("prime_rl.orchestrator.orchestrator.create_renderer", return_value=renderer) as create_renderer_mock, - patch("prime_rl.orchestrator.orchestrator.RenderingProxy", return_value=proxy) as proxy_cls_mock, - patch("prime_rl.orchestrator.orchestrator.uvicorn.Config", return_value=object()) as uvicorn_config_mock, - patch( - "prime_rl.orchestrator.orchestrator.uvicorn.Server", return_value=proxy_server - ) as uvicorn_server_mock, patch( "prime_rl.orchestrator.orchestrator.setup_inference_pool", - new=AsyncMock(return_value=upstream_pool), + new=AsyncMock(return_value=inference_pool), ) as setup_pool_mock, - patch( - "prime_rl.orchestrator.orchestrator.ProxiedInferencePool", - return_value=inference_pool, - ) as proxied_pool_mock, - patch( - "prime_rl.orchestrator.orchestrator.asyncio.create_task", return_value=proxy_task - ) as create_task_mock, ): - ( - returned_renderer, - returned_pool, - returned_proxy, - returned_server, - returned_task, - ) = await setup_rollout_inference_pool( + returned_renderer, returned_pool = await setup_rollout_inference_pool( config=config, rollout_client_config=rollout_client_config, rollout_model_name="student-model", - tokenizer=object(), - processor="processor", + tokenizer=tokenizer, logger=logger, ) assert returned_renderer is renderer assert returned_pool is inference_pool - assert returned_proxy is proxy - assert returned_server is proxy_server - assert returned_task is proxy_task - create_renderer_mock.assert_called_once() - proxy_cls_mock.assert_called_once_with( - renderer, vllm_base_url="http://localhost:8000/v1", processor="processor" + create_renderer_mock.assert_called_once_with(tokenizer, renderer="qwen3_vl") + setup_pool_mock.assert_awaited_once_with( + rollout_client_config, + model_name="student-model", + client_type="renderer", + renderer_name="qwen3_vl", ) - uvicorn_config_mock.assert_called_once() - uvicorn_server_mock.assert_called_once() - setup_pool_mock.assert_awaited_once() - proxied_pool_mock.assert_called_once() - create_task_mock.assert_called_once() asyncio.run(run()) diff --git a/tests/unit/orchestrator/test_scheduler.py b/tests/unit/orchestrator/test_scheduler.py index f537d5189ff..2c8498c29b1 100644 --- a/tests/unit/orchestrator/test_scheduler.py +++ b/tests/unit/orchestrator/test_scheduler.py @@ -7,7 +7,6 @@ from prime_rl.orchestrator.scheduler import InflightRolloutInfo, Scheduler from prime_rl.utils.async_utils import safe_cancel -from prime_rl.utils.client import RENDERER_UPSTREAM_BASE_URL_HEADER def make_scheduler() -> Scheduler: @@ -164,20 +163,14 @@ async def update_weights(weight_dir, lora_name=None, step=0) -> None: asyncio.run(run()) -def test_client_identity_distinguishes_renderer_upstreams(): +def test_client_identity_distinguishes_base_url_and_dp_rank(): client_a = vf.ClientConfig( - api_base_url="http://127.0.0.1:18100/v1", - extra_headers={ - "X-data-parallel-rank": "0", - RENDERER_UPSTREAM_BASE_URL_HEADER: "http://worker-a:8000/v1", - }, + api_base_url="http://worker-a:8000/v1", + extra_headers={"X-data-parallel-rank": "0"}, ) client_b = vf.ClientConfig( - api_base_url="http://127.0.0.1:18100/v1", - extra_headers={ - "X-data-parallel-rank": "0", - RENDERER_UPSTREAM_BASE_URL_HEADER: "http://worker-b:8000/v1", - }, + api_base_url="http://worker-a:8000/v1", + extra_headers={"X-data-parallel-rank": "1"}, ) assert Scheduler._client_identity(client_a) != Scheduler._client_identity(client_b) diff --git a/tests/unit/utils/test_client.py b/tests/unit/utils/test_client.py index 253b3987e5b..cb1b8f0c0cb 100644 --- a/tests/unit/utils/test_client.py +++ b/tests/unit/utils/test_client.py @@ -1,84 +1,71 @@ -from types import SimpleNamespace - import verifiers as vf -from prime_rl.utils.client import ( - RENDERER_UPSTREAM_BASE_URL_HEADER, - ProxiedInferencePool, -) - - -class _FakeInferencePool: - def __init__(self, clients: list[vf.ClientConfig]): - self._clients = clients - self.admin_clients = [SimpleNamespace(base_url="http://admin")] - self.wait_for_ready_calls: list[tuple[str, int]] = [] - self.update_weights_calls: list[tuple[object, str | None, int]] = [] - self.stop_calls = 0 - self.updated_model_name = None - - @property - def clients(self) -> list[vf.ClientConfig]: - return self._clients - - def update_model_name(self, model_name: str) -> None: - self.updated_model_name = model_name +from prime_rl.configs.shared import ClientConfig +from prime_rl.utils.client import setup_clients - async def get_next_client(self) -> vf.ClientConfig: - return self._clients[0] - async def wait_for_ready(self, model_name: str, timeout: int = 1800) -> None: - self.wait_for_ready_calls.append((model_name, timeout)) - - async def update_weights(self, weight_dir, lora_name: str | None = None, step: int = 0) -> None: - self.update_weights_calls.append((weight_dir, lora_name, step)) +def test_setup_clients_assigns_renderer_and_dp_rank_headers(): + client_config = ClientConfig( + base_url=["http://worker-a:8000/v1"], + api_key_var="PRIME_API_KEY", + headers={"X-Test": "test"}, + dp_rank_count=2, + extra_headers_from_state={"X-Session-ID": "session_id"}, + ) - def get_metrics(self) -> dict[str, float]: - return {"ready_servers": 2.0} + clients = setup_clients( + client_config, + client_type="renderer", + renderer_name="qwen3_vl", + ) - async def stop(self) -> None: - self.stop_calls += 1 + assert [client.client_type for client in clients] == ["renderer", "renderer"] + assert [client.renderer for client in clients] == ["qwen3_vl", "qwen3_vl"] + assert [client.renderer_model_name for client in clients] == [None, None] + assert [client.api_base_url for client in clients] == ["http://worker-a:8000/v1"] * 2 + assert [client.extra_headers["X-data-parallel-rank"] for client in clients] == ["0", "1"] + assert clients[0].extra_headers["X-Test"] == "test" + assert clients[0].extra_headers_from_state == {"X-Session-ID": "session_id"} -def _make_client(base_url: str, *, client_idx: int = 0, dp_rank: str | None = None) -> vf.ClientConfig: - headers = {} - if dp_rank is not None: - headers["X-data-parallel-rank"] = dp_rank - return vf.ClientConfig( - client_idx=client_idx, - client_type="openai_chat_completions", +def test_setup_clients_assigns_renderer_model_name(): + client_config = ClientConfig( + base_url=["http://worker-a:8000/v1"], api_key_var="PRIME_API_KEY", - api_base_url=base_url, - extra_headers=headers, - extra_headers_from_state={"X-Session-ID": "session_id"}, ) - -def test_proxied_inference_pool_wraps_clients_with_upstream_header(): - upstream_pool = _FakeInferencePool( - [ - _make_client("http://worker-a:8000/v1", client_idx=0, dp_rank="0"), - _make_client("http://worker-b:8000/v1", client_idx=1, dp_rank="0"), - ] + clients = setup_clients( + client_config, + client_type="renderer", + renderer_name="qwen3_vl", + renderer_model_name="Qwen/Qwen3-VL-4B-Instruct", ) - proxy_pool = ProxiedInferencePool(upstream_pool, proxy_base_url="http://127.0.0.1:18100/v1") - clients = proxy_pool.clients + assert clients[0].renderer_model_name == "Qwen/Qwen3-VL-4B-Instruct" - assert [client.api_base_url for client in clients] == ["http://127.0.0.1:18100/v1"] * 2 - assert [client.extra_headers[RENDERER_UPSTREAM_BASE_URL_HEADER] for client in clients] == [ - "http://worker-a:8000/v1", - "http://worker-b:8000/v1", - ] - assert clients[0].extra_headers["X-data-parallel-rank"] == "0" - assert clients[0].extra_headers_from_state == {"X-Session-ID": "session_id"} - assert upstream_pool.clients[0].api_base_url == "http://worker-a:8000/v1" - assert RENDERER_UPSTREAM_BASE_URL_HEADER not in upstream_pool.clients[0].extra_headers +def test_setup_clients_preserves_chat_client_defaults(): + client_config = ClientConfig( + base_url=["http://worker-a:8000/v1"], + api_key_var="PRIME_API_KEY", + ) -def test_proxied_inference_pool_delegates_admin_and_metrics(): - upstream_pool = _FakeInferencePool([_make_client("http://worker-a:8000/v1", client_idx=0)]) - proxy_pool = ProxiedInferencePool(upstream_pool, proxy_base_url="http://127.0.0.1:18100/v1") - - assert proxy_pool.admin_clients == upstream_pool.admin_clients - assert proxy_pool.get_metrics() == {"ready_servers": 2.0} + clients = setup_clients(client_config) + + assert clients == [ + vf.ClientConfig( + client_idx=0, + client_type="openai_chat_completions", + renderer="auto", + renderer_model_name=None, + api_key_var="PRIME_API_KEY", + api_base_url="http://worker-a:8000/v1", + timeout=client_config.timeout, + connect_timeout=client_config.connect_timeout, + max_connections=8192, + max_keepalive_connections=8192, + max_retries=10, + extra_headers={}, + extra_headers_from_state={}, + ) + ] diff --git a/tests/unit/utils/test_elastic.py b/tests/unit/utils/test_elastic.py index 38330844f80..c4bd9121451 100644 --- a/tests/unit/utils/test_elastic.py +++ b/tests/unit/utils/test_elastic.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import httpx +import verifiers as vf from prime_rl.utils.elastic import ( AdapterState, @@ -401,3 +402,46 @@ def test_get_loaded_adapter_handles_step_dash_format(): assert result is not None assert result.step == 99 + + +def test_elastic_clients_preserve_renderer_model_name_when_model_name_updates(): + with patch("prime_rl.utils.elastic.get_logger"): + client_config = MagicMock() + client_config.timeout = 1200 + client_config.connect_timeout = 30.0 + client_config.api_key_var = "PRIME_API_KEY" + client_config.headers = {} + client_config.dp_rank_count = 1 + + pool = ElasticInferencePool( + hostname="test.hostname", + client_config=client_config, + model_name="Qwen/Qwen3-VL-4B-Instruct", + client_type="renderer", + renderer_name="qwen3_vl", + port=8000, + ) + pool._servers = { + "10.0.0.1": MagicMock(status="ready"), + } + + pool.update_model_name("r8-smoke") + clients = pool.clients + + assert clients == [ + vf.ClientConfig( + client_idx=0, + client_type="renderer", + renderer="qwen3_vl", + renderer_model_name="Qwen/Qwen3-VL-4B-Instruct", + api_key_var="PRIME_API_KEY", + api_base_url="http://10.0.0.1:8000/v1", + timeout=1200, + connect_timeout=30.0, + max_connections=8192, + max_keepalive_connections=8192, + max_retries=10, + extra_headers={}, + extra_headers_from_state={}, + ) + ] diff --git a/uv.lock b/uv.lock index 015549bc9a2..724ab07ad7e 100644 --- a/uv.lock +++ b/uv.lock @@ -3231,13 +3231,7 @@ dependencies = [ ] [package.metadata] -requires-dist = [ - { name = "httpx", marker = "extra == 'proxy'", specifier = ">=0.27.0" }, - { name = "starlette", marker = "extra == 'proxy'", specifier = ">=0.40.0" }, - { name = "transformers", specifier = ">=4.50.0" }, - { name = "uvicorn", marker = "extra == 'proxy'", specifier = ">=0.30.0" }, -] -provides-extras = ["proxy"] +requires-dist = [{ name = "transformers", specifier = ">=4.50.0" }] [[package]] name = "requests" From 4efa271d00890c3a0a9511919dcef519edf40064 Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 14 Apr 2026 16:33:48 +0000 Subject: [PATCH 08/54] feat: add new renderer options to config docstring List deepseek_v3, kimi_k2, kimi_k25, nemotron3, gpt_oss as available renderer choices in the BaseModelConfig.renderer field description. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/prime_rl/configs/shared.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/prime_rl/configs/shared.py b/src/prime_rl/configs/shared.py index 259eef61612..f04c906220a 100644 --- a/src/prime_rl/configs/shared.py +++ b/src/prime_rl/configs/shared.py @@ -129,7 +129,8 @@ class BaseModelConfig(BaseConfig): description=( "Renderer to use for chat template tokenization. " "Options: 'auto' (detect from tokenizer), 'qwen3', 'qwen3_vl', " - "'qwen3.5', 'glm5', 'glm4.5', 'minimax-m2', 'default'." + "'qwen3.5', 'glm5', 'glm4.5', 'minimax-m2', 'deepseek_v3', " + "'kimi_k2', 'kimi_k25', 'nemotron3', 'gpt_oss', 'default'." ), ), ] = "auto" From 7a7dcc60e3f2e55b4ffa05330aa409188c08ea67 Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 14 Apr 2026 16:45:47 +0000 Subject: [PATCH 09/54] fix: point verifiers dep at renderers branch instead of local path Co-Authored-By: Claude Opus 4.6 (1M context) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cd8be303c5e..f8dfb6898f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,7 +120,7 @@ override-dependencies = [ [tool.uv.sources] torch = { index = "pytorch-cu128" } -verifiers = { path = "/home/ubuntu/verifiers", editable = true } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", branch = "renderers" } torchtitan = { git = "https://github.com/pytorch/torchtitan", rev = "a1fdd7e" } dion = { git = "https://github.com/samsja/dion.git", rev = "d891eeb" } transformers = { git = "https://github.com/huggingface/transformers.git", rev = "c1c3424" } From d1506f503669bd2c4c6e30707c81a25383b006a9 Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 14 Apr 2026 17:18:37 +0000 Subject: [PATCH 10/54] chore: remove debug smoke configs Co-Authored-By: Claude Opus 4.6 (1M context) --- configs/debug/rl_lora_smoke.toml | 42 ------------------- .../teacher_rollout_hard_distill_smoke.toml | 37 ---------------- 2 files changed, 79 deletions(-) delete mode 100644 configs/debug/rl_lora_smoke.toml delete mode 100644 configs/debug/teacher_rollout_hard_distill_smoke.toml diff --git a/configs/debug/rl_lora_smoke.toml b/configs/debug/rl_lora_smoke.toml deleted file mode 100644 index dd47ab0cf8f..00000000000 --- a/configs/debug/rl_lora_smoke.toml +++ /dev/null @@ -1,42 +0,0 @@ -max_steps = 4 -max_async_level = 0 -seq_len = 1024 -output_dir = "outputs/rl_lora_smoke" - -[ckpt] -interval = 1 - -[deployment] -type = "single_node" -gpus_per_node = 2 -num_train_gpus = 1 -num_infer_gpus = 1 - -[model] -name = "PrimeIntellect/Qwen3-0.6B-Reverse-Text-SFT" - -[trainer.optim] -lr = 5e-5 - -[trainer.model.lora] -rank = 8 - -[trainer.ckpt.weights] -save_adapter_separately = true - -[orchestrator] -batch_size = 16 -rollouts_per_example = 4 - -[orchestrator.model.lora] -name = "r8-smoke" - -[orchestrator.sampling] -max_tokens = 128 - -[[orchestrator.env]] -id = "reverse-text" - -[inference] -enable_lora = true -gpu_memory_utilization = 0.7 diff --git a/configs/debug/teacher_rollout_hard_distill_smoke.toml b/configs/debug/teacher_rollout_hard_distill_smoke.toml deleted file mode 100644 index d83b3fe605d..00000000000 --- a/configs/debug/teacher_rollout_hard_distill_smoke.toml +++ /dev/null @@ -1,37 +0,0 @@ -max_steps = 1 -seq_len = 1024 -output_dir = "outputs/teacher_rollout_hard_distill_smoke" - -[deployment] -type = "single_node" -gpus_per_node = 2 -num_train_gpus = 1 -num_infer_gpus = 0 - -[model] -name = "PrimeIntellect/Qwen3-0.6B-Reverse-Text-SFT" - -[trainer.loss] -type = "sft" - -[trainer.optim] -lr = 1e-5 - -[orchestrator] -batch_size = 8 -rollouts_per_example = 2 - -[orchestrator.sampling] -max_tokens = 64 -temperature = 0.7 - -[orchestrator.teacher_rollout_model.client] -base_url = ["https://api.pinference.ai/api/v1"] -api_key_var = "PRIME_API_KEY" -skip_model_check = true - -[orchestrator.teacher_rollout_model.model] -name = "PrimeIntellect/Qwen3-0.6B-Reverse-Text-SFT" - -[[orchestrator.env]] -id = "reverse-text" From b53242819c0439ca0a4e8df9cdf0a9f94ccb38f9 Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 14 Apr 2026 17:41:30 +0000 Subject: [PATCH 11/54] fix: remove old TITO endpoint, fix merge import gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove /v1/chat/completions/tokens endpoint and serving_chat_with_tokens — replaced entirely by /v1/generate with client-side renderer tokenization. Fix missing HTTPStatus and ChatCompletionResponse imports from merge. Tested: reverse-text (Qwen3-0.6B) and color-codeword VLM (Qwen3-VL-4B) both complete 3 RL steps successfully. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/prime_rl/inference/vllm/server.py | 30 +--------- uv.lock | 85 +++------------------------ 2 files changed, 9 insertions(+), 106 deletions(-) diff --git a/src/prime_rl/inference/vllm/server.py b/src/prime_rl/inference/vllm/server.py index d63ecec9f6e..02e2eafcded 100644 --- a/src/prime_rl/inference/vllm/server.py +++ b/src/prime_rl/inference/vllm/server.py @@ -2,8 +2,8 @@ from typing import Any import uvloop -from fastapi import APIRouter, Depends, Request -from fastapi.responses import JSONResponse, StreamingResponse +from fastapi import APIRouter, Request +from fastapi.responses import JSONResponse from starlette.datastructures import State from vllm.engine.protocol import EngineClient from vllm.entrypoints.openai.api_server import init_app_state @@ -247,32 +247,6 @@ async def init_broadcaster(request: Request): return {"status": "ok"} -@router.post( - "/v1/chat/completions/tokens", - dependencies=[Depends(validate_json_request)], - responses={ - HTTPStatus.OK.value: {"content": {"text/event-stream": {}}}, - HTTPStatus.BAD_REQUEST.value: {"model": ErrorResponse}, - HTTPStatus.NOT_FOUND.value: {"model": ErrorResponse}, - HTTPStatus.INTERNAL_SERVER_ERROR.value: {"model": ErrorResponse}, - }, -) -@with_cancellation -@load_aware_call -async def _chat_with_tokens(request: ChatCompletionRequestWithTokens, raw_request: Request): - handler = chat_with_tokens(raw_request) - if handler is None: - return base(raw_request).create_error_response(message="The model does not support Chat Completions API") - generator = await handler.create_chat_completion_with_tokens(request, raw_request) - if isinstance(generator, ErrorResponse): - return JSONResponse(content=generator.model_dump(), status_code=generator.error.code) - - elif isinstance(generator, ChatCompletionResponse): - return JSONResponse(content=generator.model_dump()) - - return StreamingResponse(content=generator, media_type="text/event-stream") - - async def custom_init_app_state( engine_client: EngineClient, state: State, diff --git a/uv.lock b/uv.lock index 843dd5caed4..ee41c1a3524 100644 --- a/uv.lock +++ b/uv.lock @@ -2787,7 +2787,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", editable = "../verifiers" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?branch=renderers" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.20/vllm_router-0.1.20-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -2824,16 +2824,16 @@ wheels = [ [[package]] name = "prime-tunnel" -version = "0.1.5" +version = "0.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "pydantic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "tenacity", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/d2/17236f43b855a4261252d317ef230beb0571268427418c8d471d3eba195c/prime_tunnel-0.1.5.tar.gz", hash = "sha256:eec296f92abbd761edcb779db7052bf9ce9c3469641f8f17645b7ed5c478257e", size = 12609, upload-time = "2026-03-21T00:52:56.147Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/e8/fec186cfbcc670deed94a7b5ace67688feed38b114b1c602d35a8f8d7564/prime_tunnel-0.1.6.tar.gz", hash = "sha256:5e371bc3557d8a5257a43ef1aeea1f8918da966a8565dacd33a647b6c82a0eaa", size = 13019, upload-time = "2026-04-13T15:13:17.24Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/d2/26c801e0ee8ed867875a704c1fa238581f505bbd96f618c894c8dca991fc/prime_tunnel-0.1.5-py3-none-any.whl", hash = "sha256:fc24bf60d8023f53850be9602dc7d0e1be3e091a019a06c65d32a6b3d61754af", size = 14758, upload-time = "2026-03-21T00:52:55.278Z" }, + { url = "https://files.pythonhosted.org/packages/52/35/adfac88e5880418f095758729b73e395c81be869d0be786fcd4b31938b2f/prime_tunnel-0.1.6-py3-none-any.whl", hash = "sha256:bc096bca509621379f671e2401253ab2ebe6c8c143d797b6ef85d8290c462e0d", size = 14914, upload-time = "2026-04-13T15:13:16.08Z" }, ] [[package]] @@ -3238,14 +3238,11 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { editable = "../verifiers/packages/renderers" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&branch=renderers#fa5f015ce9f93dc45aa442a68921bc6c1a00ab3e" } dependencies = [ { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -[package.metadata] -requires-dist = [{ name = "transformers", specifier = ">=4.50.0" }] - [[package]] name = "requests" version = "2.32.5" @@ -4035,7 +4032,8 @@ wheels = [ [[package]] name = "verifiers" -source = { editable = "../verifiers" } +version = "0.1.12.dev5" +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?branch=renderers#fa5f015ce9f93dc45aa442a68921bc6c1a00ab3e" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -4064,75 +4062,6 @@ dependencies = [ { name = "wget", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -[package.metadata] -requires-dist = [ - { name = "accelerate", marker = "extra == 'rl'", specifier = ">=1.4.0" }, - { name = "aiohttp", marker = "extra == 'browser'", specifier = ">=3.9.0" }, - { name = "aiolimiter", specifier = ">=1.2.1" }, - { name = "anthropic", specifier = ">=0.78.0" }, - { name = "datasets", specifier = ">=3.0.0,<4.7.0" }, - { name = "deepspeed", marker = "extra == 'rl'", specifier = ">=0.17.6" }, - { name = "flash-attn", marker = "extra == 'rl'", specifier = ">=2.8.3" }, - { name = "gepa" }, - { name = "httpx", specifier = ">=0.27.0" }, - { name = "jinja2", specifier = ">=3.1.6" }, - { name = "liger-kernel", marker = "extra == 'rl'", specifier = ">=0.5.10" }, - { name = "math-verify", specifier = ">=0.8.0" }, - { name = "mcp", specifier = ">=1.14.1" }, - { name = "msgpack", specifier = ">=1.1.2" }, - { name = "nest-asyncio", specifier = ">=1.6.0" }, - { name = "nltk", marker = "extra == 'ta'" }, - { name = "numpy" }, - { name = "openai", specifier = ">=1.108.1" }, - { name = "openai-agents", specifier = ">=0.0.7" }, - { name = "openenv-core", extras = ["core"], marker = "extra == 'openenv'", specifier = "==0.2.1" }, - { name = "peft", marker = "extra == 'rl'" }, - { name = "prime-sandboxes", specifier = ">=0.2.19" }, - { name = "prime-tunnel", specifier = ">=0.1.5" }, - { name = "pydantic", specifier = ">=2.11.9" }, - { name = "python-dotenv", marker = "extra == 'browser'", specifier = ">=1.0.0" }, - { name = "pyzmq", specifier = ">=27.1.0" }, - { name = "reasoning-gym", marker = "extra == 'rg'" }, - { name = "regex", specifier = "<2026.4.4" }, - { name = "renderers", editable = "../verifiers/packages/renderers" }, - { name = "requests" }, - { name = "requests", marker = "extra == 'rl'" }, - { name = "rich" }, - { name = "setproctitle", specifier = ">=1.3.0" }, - { name = "stagehand", marker = "extra == 'browser'", specifier = ">=3.0.0" }, - { name = "tenacity", specifier = ">=8.5.0" }, - { name = "textarena", marker = "extra == 'ta'" }, - { name = "textual" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "torch", marker = "extra == 'rl'", specifier = ">=2.8.0,<2.9.0" }, - { name = "transformers", marker = "extra == 'rl'", specifier = ">=4.56.2" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, - { name = "vllm", marker = "extra == 'rl'", specifier = ">=0.10.0,<0.11.0" }, - { name = "wandb", marker = "extra == 'rl'" }, - { name = "wget", specifier = ">=3.2" }, -] -provides-extras = ["browser", "openenv", "rg", "rl", "ta"] - -[package.metadata.requires-dev] -dev = [ - { name = "aiohttp", specifier = ">=3.9.0" }, - { name = "ipykernel" }, - { name = "ipywidgets" }, - { name = "nltk" }, - { name = "openenv-core", extras = ["core"], specifier = "==0.2.1" }, - { name = "pre-commit" }, - { name = "pytest", specifier = ">=7.0.0" }, - { name = "pytest-asyncio", specifier = ">=0.21.0" }, - { name = "pytest-cov", specifier = ">=4.0.0" }, - { name = "pytest-xdist", specifier = ">=3.8.0" }, - { name = "python-dotenv", specifier = ">=1.0.0" }, - { name = "reasoning-gym" }, - { name = "ruff" }, - { name = "stagehand", specifier = ">=3.0.0" }, - { name = "textarena" }, - { name = "ty", specifier = ">=0.0.1a29,<0.0.22" }, -] - [[package]] name = "virtualenv" version = "20.34.0" From 67ffcb5265da468e20cd5796706868843f55c678 Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 14 Apr 2026 17:46:44 +0000 Subject: [PATCH 12/54] refactor: remove OpenAIServingChatWithRoutedExperts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dead code — routed experts are captured natively by the /v1/generate endpoint (serving_generate.py). The eval path (/v1/chat/completions) uses standard vLLM chat serving and doesn't need routed experts. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/prime_rl/inference/vllm/server.py | 17 +---- .../vllm/serving_chat_with_routed_experts.py | 73 ------------------- 2 files changed, 3 insertions(+), 87 deletions(-) delete mode 100644 src/prime_rl/inference/vllm/serving_chat_with_routed_experts.py diff --git a/src/prime_rl/inference/vllm/server.py b/src/prime_rl/inference/vllm/server.py index 02e2eafcded..5461a58e8a7 100644 --- a/src/prime_rl/inference/vllm/server.py +++ b/src/prime_rl/inference/vllm/server.py @@ -136,9 +136,6 @@ def resolve_tool_call_parser(model_name: str, tool_call_parser: str | None) -> s monkey_patch_load_lora_adapter, monkey_patch_tokenize_params_validation, ) -from prime_rl.inference.vllm.serving_chat_with_routed_experts import ( - OpenAIServingChatWithRoutedExperts, -) # NOTE: Fix harmony stop token propagation for GPT-OSS models # Upstream issue still open: https://github.com/vllm-project/vllm/issues/22519 @@ -256,23 +253,15 @@ async def custom_init_app_state( """ Modifies init_app_state: 1. Call the original init_app_state to set up standard state. - 2. Replace the serving_chat with our routed-experts wrapper so chat responses - can still expose routed experts when requested. + 2. Add /v1/generate endpoint for renderer-based token-level inference. """ await init_app_state(engine_client, state, args, supported_tasks) - if "generate" in supported_tasks and state.openai_serving_chat is not None: - original_chat = state.openai_serving_chat - serving_chat = object.__new__(OpenAIServingChatWithRoutedExperts) - serving_chat.__dict__.update(original_chat.__dict__) - state.openai_serving_chat = serving_chat - else: - serving_chat = None - # /v1/generate endpoint — tokens + optional images, no chat template from prime_rl.inference.vllm.serving_generate import OpenAIServingGenerate - state.openai_serving_generate = OpenAIServingGenerate(engine_client, chat_handler=serving_chat) + chat_handler = state.openai_serving_chat if "generate" in supported_tasks else None + state.openai_serving_generate = OpenAIServingGenerate(engine_client, chat_handler=chat_handler) import vllm.entrypoints.openai.api_server diff --git a/src/prime_rl/inference/vllm/serving_chat_with_routed_experts.py b/src/prime_rl/inference/vllm/serving_chat_with_routed_experts.py deleted file mode 100644 index d1ebefda387..00000000000 --- a/src/prime_rl/inference/vllm/serving_chat_with_routed_experts.py +++ /dev/null @@ -1,73 +0,0 @@ -import base64 -from collections.abc import AsyncGenerator, AsyncIterator - -import numpy as np -from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest, ChatCompletionResponse -from vllm.entrypoints.openai.chat_completion.serving import OpenAIServingChat -from vllm.entrypoints.openai.engine.protocol import ErrorResponse, RequestResponseMetadata -from vllm.logger import init_logger -from vllm.outputs import RequestOutput -from vllm.reasoning import ReasoningParser - -logger = init_logger(__name__) - - -class _RoutedExpertsCapture: - def __init__(self, generator: AsyncGenerator[RequestOutput, None]): - self._generator = generator - self.routed_experts: dict[int, dict] = {} - - def _encode_routed_experts(self, arr: np.ndarray) -> dict: - return { - "data": base64.b85encode(arr.tobytes()).decode("ascii"), - "shape": list(arr.shape), - } - - async def __aiter__(self): - async for request_output in self._generator: - for output in request_output.outputs: - if output.routed_experts is not None: - self.routed_experts[output.index] = self._encode_routed_experts(output.routed_experts) - yield request_output - - def post_process(self, response: ChatCompletionResponse): - for choice in response.choices: - if choice.index in self.routed_experts: - choice.routed_experts = self.routed_experts[choice.index] - - -class OpenAIServingChatWithRoutedExperts(OpenAIServingChat): - """OpenAI chat serving wrapper that preserves routed expert metadata.""" - - async def chat_completion_full_generator( - self, - request: ChatCompletionRequest, - result_generator: AsyncIterator[RequestOutput], - request_id: str, - model_name: str, - conversation, - tokenizer, - request_metadata: RequestResponseMetadata, - reasoning_parser: ReasoningParser | None = None, - ) -> ErrorResponse | ChatCompletionResponse: - if self.model_config.enable_return_routed_experts: - capture = _RoutedExpertsCapture(result_generator) - result_generator = capture - else: - capture = None - - response = await super().chat_completion_full_generator( - request, - result_generator, - request_id, - model_name, - conversation, - tokenizer, - request_metadata, - reasoning_parser, - ) - - if capture and isinstance(response, ChatCompletionResponse): - capture.post_process(response) - - return response From 753d6161052b2e5bf4d035b037ee03d4677f9bfc Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 14 Apr 2026 20:54:27 +0000 Subject: [PATCH 13/54] fix: update tests for new train/eval client type split - test_orchestrator_setup: assert train_client_type/eval_client_type instead of single client_type - test_teacher_logprobs: remove get_semaphore monkeypatch (removed in compute_teacher_logprobs refactor) - test_elastic: use client_config with .elastic.hostname instead of passing hostname as kwarg (matches current ElasticInferencePool API) Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/unit/orchestrator/test_orchestrator_setup.py | 3 ++- tests/unit/orchestrator/test_teacher_logprobs.py | 11 ----------- tests/unit/utils/test_elastic.py | 8 +++++--- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/tests/unit/orchestrator/test_orchestrator_setup.py b/tests/unit/orchestrator/test_orchestrator_setup.py index a0f4d669f11..4c96878686c 100644 --- a/tests/unit/orchestrator/test_orchestrator_setup.py +++ b/tests/unit/orchestrator/test_orchestrator_setup.py @@ -70,7 +70,8 @@ async def run() -> None: setup_pool_mock.assert_awaited_once_with( rollout_client_config, model_name="student-model", - client_type="renderer", + train_client_type="renderer", + eval_client_type="openai_chat_completions", renderer_name="qwen3_vl", ) diff --git a/tests/unit/orchestrator/test_teacher_logprobs.py b/tests/unit/orchestrator/test_teacher_logprobs.py index 8cb94c5eb11..e126805a81a 100644 --- a/tests/unit/orchestrator/test_teacher_logprobs.py +++ b/tests/unit/orchestrator/test_teacher_logprobs.py @@ -1,5 +1,4 @@ import asyncio -from contextlib import asynccontextmanager import verifiers as vf @@ -24,21 +23,11 @@ async def post(self, path, *, cast_to, body): ) -@asynccontextmanager -async def _null_semaphore(): - yield - - def test_compute_teacher_logprobs_uses_generate_endpoint(monkeypatch): async def _run(): fake_client = _FakeOpenAIClient() monkeypatch.setattr(orchestrator_utils, "setup_openai_client", lambda _: fake_client) - async def _fake_get_semaphore(): - return _null_semaphore() - - monkeypatch.setattr(orchestrator_utils, "get_semaphore", _fake_get_semaphore) - sample = TrainingSample( prompt_ids=[1], prompt_mask=[True], diff --git a/tests/unit/utils/test_elastic.py b/tests/unit/utils/test_elastic.py index 74125b17801..846f52a2cfe 100644 --- a/tests/unit/utils/test_elastic.py +++ b/tests/unit/utils/test_elastic.py @@ -407,6 +407,10 @@ def test_get_loaded_adapter_handles_step_dash_format(): def test_elastic_clients_preserve_renderer_model_name_when_model_name_updates(): with patch("prime_rl.utils.elastic.get_logger"): client_config = MagicMock() + client_config.elastic.hostname = "test.hostname" + client_config.elastic.port = 8000 + client_config.elastic.sync_interval = 5.0 + client_config.router_url = None client_config.timeout = 1200 client_config.connect_timeout = 30.0 client_config.api_key_var = "PRIME_API_KEY" @@ -414,12 +418,10 @@ def test_elastic_clients_preserve_renderer_model_name_when_model_name_updates(): client_config.dp_rank_count = 1 pool = ElasticInferencePool( - hostname="test.hostname", client_config=client_config, model_name="Qwen/Qwen3-VL-4B-Instruct", - client_type="renderer", + train_client_type="renderer", renderer_name="qwen3_vl", - port=8000, ) pool._servers = { "10.0.0.1": MagicMock(status="ready"), From 9f84ce3a7f527b8bedf37a2422d325c958aa8143 Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 14 Apr 2026 20:57:30 +0000 Subject: [PATCH 14/54] feat: default train_client_type to renderer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renderers are the standard training path — all tokenization happens client-side. External teacher models still use openai_chat_completions explicitly where needed. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/prime_rl/utils/client.py | 4 ++-- src/prime_rl/utils/elastic.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/prime_rl/utils/client.py b/src/prime_rl/utils/client.py index c2fc08d9d51..739a4de0522 100644 --- a/src/prime_rl/utils/client.py +++ b/src/prime_rl/utils/client.py @@ -62,7 +62,7 @@ def __init__( self, client_config: ClientConfig, model_name: str, - train_client_type: str = "openai_chat_completions", + train_client_type: str = "renderer", eval_client_type: str = "openai_chat_completions", renderer_name: str = "auto", ): @@ -114,7 +114,7 @@ async def stop(self) -> None: async def setup_inference_pool( client_config: ClientConfig, model_name: str, - train_client_type: str = "openai_chat_completions", + train_client_type: str = "renderer", eval_client_type: str = "openai_chat_completions", renderer_name: str = "auto", ) -> InferencePool: diff --git a/src/prime_rl/utils/elastic.py b/src/prime_rl/utils/elastic.py index e1c8c33c0de..4f0168b4a36 100644 --- a/src/prime_rl/utils/elastic.py +++ b/src/prime_rl/utils/elastic.py @@ -104,7 +104,7 @@ def __init__( self, client_config: ClientConfig, model_name: str, - train_client_type: str = "openai_chat_completions", + train_client_type: str = "renderer", eval_client_type: str = "openai_chat_completions", renderer_name: str = "auto", ): @@ -140,7 +140,7 @@ async def from_config( cls, client_config: ClientConfig, model_name: str, - train_client_type: str = "openai_chat_completions", + train_client_type: str = "renderer", eval_client_type: str = "openai_chat_completions", renderer_name: str = "auto", ) -> ElasticInferencePool: From 920f381b837bf2ff60b51104758e90465f586c46 Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 14 Apr 2026 21:25:27 +0000 Subject: [PATCH 15/54] fix: elastic test uses train_clients instead of removed clients property Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/unit/utils/test_elastic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/utils/test_elastic.py b/tests/unit/utils/test_elastic.py index 846f52a2cfe..9e08e0f7519 100644 --- a/tests/unit/utils/test_elastic.py +++ b/tests/unit/utils/test_elastic.py @@ -428,7 +428,7 @@ def test_elastic_clients_preserve_renderer_model_name_when_model_name_updates(): } pool.update_model_name("r8-smoke") - clients = pool.clients + clients = pool.train_clients assert clients == [ vf.ClientConfig( From 208a9b3b505b84adf617d4b0274a0a43a0349d31 Mon Sep 17 00:00:00 2001 From: hallerite Date: Wed, 15 Apr 2026 13:52:32 +0000 Subject: [PATCH 16/54] chore: pin verifiers renderer revision --- pyproject.toml | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 07e94776561..f5412b4bd0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,6 @@ override-dependencies = [ [tool.uv.sources] torch = { index = "pytorch-cu128" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", branch = "renderers" } torchtitan = { git = "https://github.com/pytorch/torchtitan", rev = "a1fdd7e" } dion = { git = "https://github.com/samsja/dion.git", rev = "d891eeb" } transformers = { git = "https://github.com/huggingface/transformers.git", rev = "c1c3424" } @@ -149,6 +148,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "acd7f53c93f93b95473dcc2dced5a09d762dc7dd" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] diff --git a/uv.lock b/uv.lock index ee41c1a3524..3e606cf1eb2 100644 --- a/uv.lock +++ b/uv.lock @@ -2787,7 +2787,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?branch=renderers" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=acd7f53c93f93b95473dcc2dced5a09d762dc7dd" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.20/vllm_router-0.1.20-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3238,7 +3238,7 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&branch=renderers#fa5f015ce9f93dc45aa442a68921bc6c1a00ab3e" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=acd7f53c93f93b95473dcc2dced5a09d762dc7dd#acd7f53c93f93b95473dcc2dced5a09d762dc7dd" } dependencies = [ { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] @@ -4033,7 +4033,7 @@ wheels = [ [[package]] name = "verifiers" version = "0.1.12.dev5" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?branch=renderers#fa5f015ce9f93dc45aa442a68921bc6c1a00ab3e" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=acd7f53c93f93b95473dcc2dced5a09d762dc7dd#acd7f53c93f93b95473dcc2dced5a09d762dc7dd" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From 55d44d083a6f1c3994a86b57f1a3708c4906b8c5 Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 21 Apr 2026 13:41:35 +0000 Subject: [PATCH 17/54] chore: bump verifiers to c4e50e4 (Qwen3-VL image-pad fix) Picks up the renderers branch fix that stops the Qwen3-VL renderer from double-expanding <|image_pad|> placeholders when sending to /v1/generate. Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 594e67d6e06..0d1d2361b1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "ca41eca" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "c4e50e4" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] diff --git a/uv.lock b/uv.lock index 7ae2a836dea..f3d6e06c7da 100644 --- a/uv.lock +++ b/uv.lock @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=ca41eca" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=c4e50e4" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3249,7 +3249,7 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=ca41eca#ca41eca3d45a76b8acab8b4d7e4e3fa25f48f07d" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=c4e50e4#c4e50e4e2ab6591e697f549c43b79027a904a760" } dependencies = [ { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] @@ -4044,7 +4044,7 @@ wheels = [ [[package]] name = "verifiers" version = "0.1.12.dev5" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=ca41eca#ca41eca3d45a76b8acab8b4d7e4e3fa25f48f07d" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=c4e50e4#c4e50e4e2ab6591e697f549c43b79027a904a760" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From e9cb682bc01520b603c529c96b24166d9a0ef8ce Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 21 Apr 2026 13:48:58 +0000 Subject: [PATCH 18/54] chore: bump verifiers to 2d8cc29 (trainer/vLLM image-pad fix) Picks up the renderer-client fix that returns vLLM's server-expanded prompt_token_ids so the trainer replays the same sequence vLLM ran on (un-expanded pads were failing trainer forward with a tokens/features mismatch on multi-image turns). Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0d1d2361b1e..e0ffd3332a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "c4e50e4" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "2d8cc29" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] diff --git a/uv.lock b/uv.lock index f3d6e06c7da..c3324d075df 100644 --- a/uv.lock +++ b/uv.lock @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=c4e50e4" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=2d8cc29" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3249,7 +3249,7 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=c4e50e4#c4e50e4e2ab6591e697f549c43b79027a904a760" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=2d8cc29#2d8cc292b65ee447064e827ae2bfeaa3d822e250" } dependencies = [ { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] @@ -4044,7 +4044,7 @@ wheels = [ [[package]] name = "verifiers" version = "0.1.12.dev5" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=c4e50e4#c4e50e4e2ab6591e697f549c43b79027a904a760" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=2d8cc29#2d8cc292b65ee447064e827ae2bfeaa3d822e250" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From e781e9cdfa82815d01fb047d34bb56ec3e0d79fd Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 21 Apr 2026 14:16:45 +0000 Subject: [PATCH 19/54] fix(vlm): expand <|image_pad|> in trainer samples in sync with parallel arrays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The renderer-based path stores un-expanded image placeholders in the trajectory so that vLLM's /v1/generate can expand them once against multi_modal_data (sending pre-expanded placeholders triggers a double-expansion in vLLM's mm preprocessor and scrambles image features on any turn past the first). But the trainer's Qwen3-VL forward needs the placeholders already expanded 1:1 with image features, otherwise it aborts with "Image features and image tokens do not match, tokens: N, features: M". Expand each <|image_pad|> in place using the processor's image_grid_thw at the point we attach pixel_values to the TrainingSample — walking ids, mask, logprobs, and temperatures together so every parallel array stays aligned with the expanded token stream (extension-path positions were already mask=False / lp=0.0, so inserted pad slots inherit those). Plumbs the processor argument through interleave_rollout so the helper can read image_token_id and merge_size off it at the call site. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/prime_rl/orchestrator/orchestrator.py | 6 +- src/prime_rl/orchestrator/trajectories.py | 101 ++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/prime_rl/orchestrator/orchestrator.py b/src/prime_rl/orchestrator/orchestrator.py index fea7ed72b93..3c051986d4d 100644 --- a/src/prime_rl/orchestrator/orchestrator.py +++ b/src/prime_rl/orchestrator/orchestrator.py @@ -506,7 +506,11 @@ async def orchestrate(config: OrchestratorConfig): # Process rollouts in parallel def process_rollout(rollout: vf.RolloutOutput, rollout_idx: int) -> list[TrainingSample] | None: return interleave_rollout( - rollout, vlm_cache=vlm_cache, cache_key=rollout_idx, mm_token_type_ids_mapping=mm_token_type_ids_mapping + rollout, + vlm_cache=vlm_cache, + cache_key=rollout_idx, + mm_token_type_ids_mapping=mm_token_type_ids_mapping, + processor=processor, ) results = await asyncio.gather( diff --git a/src/prime_rl/orchestrator/trajectories.py b/src/prime_rl/orchestrator/trajectories.py index fb60bf5cd5c..4bbc8df4ced 100644 --- a/src/prime_rl/orchestrator/trajectories.py +++ b/src/prime_rl/orchestrator/trajectories.py @@ -249,11 +249,100 @@ def pretokenize_rollout_trajectory( return True +def _expand_vlm_image_placeholders( + sample: TrainingSample, + image_grid_thw: list[list[int]], + *, + image_token_id: int, + merge_size: int, + temperature: float, +) -> None: + """In place: replace each un-expanded `<|image_pad|>` in a TrainingSample + with N copies matching the per-image patch count. + + The Qwen3-VL renderer emits one un-expanded placeholder per image so vLLM + can expand them server-side against ``multi_modal_data``. The trainer + forward pass, by contrast, expects placeholders pre-expanded 1:1 with + image features. Every parallel per-token array on the sample (mask, + logprobs, temperatures, routed_experts) has to be padded at the exact + same positions or the training loss lines up with the wrong tokens. + + Idempotent: if the sample already carries expanded placeholders (total + pad count already matches the feature count) we return immediately. Any + other count triggers a hard ``ValueError`` rather than silent drift. + """ + denom = merge_size * merge_size + expected_features = sum(max(t * h * w // denom, 1) for t, h, w in image_grid_thw) + expected_images = len(image_grid_thw) + total_pads = sample.prompt_ids.count(image_token_id) + sample.completion_ids.count(image_token_id) + if total_pads == 0 or total_pads == expected_features: + return + if total_pads != expected_images: + raise ValueError(f"Expected {expected_images} image placeholder(s) to match image_grid_thw, found {total_pads}") + + grid_iter = iter(image_grid_thw) + + # Prompt stream: only ids and prompt_mask are per-token. + new_prompt_ids: list[int] = [] + new_prompt_mask: list[bool] = [] + for tok, masked in zip(sample.prompt_ids, sample.prompt_mask, strict=True): + if tok == image_token_id: + t, h, w = next(grid_iter) + count = max(t * h * w // denom, 1) + new_prompt_ids.extend([image_token_id] * count) + new_prompt_mask.extend([masked] * count) + else: + new_prompt_ids.append(tok) + new_prompt_mask.append(masked) + + # Completion stream: ids + mask + logprobs + temperatures stay in lockstep. + # Inserted pad positions are not gradient-tracked and carry no real + # logprob; the extension path already uses mask=False / lp=0.0 for them. + new_comp_ids: list[int] = [] + new_comp_mask: list[bool] = [] + new_comp_lp: list[float] = [] + new_comp_temps: list[float] = [] + for tok, masked, lp, temp in zip( + sample.completion_ids, + sample.completion_mask, + sample.completion_logprobs, + sample.completion_temperatures, + strict=True, + ): + if tok == image_token_id: + t, h, w = next(grid_iter) + count = max(t * h * w // denom, 1) + new_comp_ids.extend([image_token_id] * count) + new_comp_mask.extend([False] * count) + new_comp_lp.extend([0.0] * count) + new_comp_temps.extend([temperature] * count) + else: + new_comp_ids.append(tok) + new_comp_mask.append(masked) + new_comp_lp.append(lp) + new_comp_temps.append(temp) + + sample.prompt_ids = new_prompt_ids + sample.prompt_mask = new_prompt_mask + sample.completion_ids = new_comp_ids + sample.completion_mask = new_comp_mask + sample.completion_logprobs = new_comp_lp + sample.completion_temperatures = new_comp_temps + # VLM rollouts don't ship routed-experts data (dense Qwen3-VL); re-align + # if some future path does provide it so downstream shape checks hold. + if sample.routed_experts is not None: + sample.routed_experts = _align_routed_experts( + sample.routed_experts, + len(sample.prompt_ids) + len(sample.completion_ids), + ) + + def interleave_rollout( output: vf.RolloutOutput, vlm_cache: "VLMImageCache | None" = None, cache_key: int | None = None, mm_token_type_ids_mapping: dict[int, int] | None = None, + processor: Any | None = None, ) -> list[TrainingSample] | None: """ Convert vf.RolloutOutput to trainable rollouts by interleaving trajectory steps @@ -409,11 +498,23 @@ def extend_sample(sample: TrainingSample, prefix_len: int, step_idx: int) -> Non # Attach images once per sample using only the last merged step if vlm_cache is not None: key = output["example_id"] if cache_key is None else cache_key + image_token_id = getattr(processor, "image_token_id", None) if processor is not None else None + merge_size = ( + getattr(getattr(processor, "image_processor", None), "merge_size", None) if processor is not None else None + ) for _, sample, last_step_idx in active_samples: pv, shape, grids = vlm_cache.get_for_step(key, last_step_idx) sample.pixel_values = pv sample.pixel_values_shape = shape sample.image_grid_thw = grids + if grids and image_token_id is not None and merge_size: + _expand_vlm_image_placeholders( + sample, + grids, + image_token_id=image_token_id, + merge_size=merge_size, + temperature=temperature, + ) if mm_token_type_ids_mapping is not None: sample.mm_token_type_ids = [ mm_token_type_ids_mapping.get(token_id, 0) for token_id in sample.prompt_ids + sample.completion_ids From 160d4c90d1e5be157333c621f03c3003a1ce6b63 Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 21 Apr 2026 14:45:18 +0000 Subject: [PATCH 20/54] refactor(vlm): model-agnostic image-pad expansion + overlap preprocessing Replace the grid_thw-specific expansion helper with a model-agnostic one that takes a per-image soft-token count (`tokens_per_image: list[int]`). For Qwen-family VLMs the caller derives it via ``_tokens_per_image_from_grid_thw(grids, merge_size)``; Gemma-family processors expose ``num_soft_tokens_per_image`` directly and don't need the grid_thw geometry at all. This keeps both paths on one code path. Also overlap the per-step preprocessing stage: pretokenize each rollout on a thread (was a blocking for-loop) and gather it together with the VLM image cache build, so they run concurrently and yield the event loop. With max_async_level >= 2 this overlaps with inference for the next batch instead of serializing behind it. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/prime_rl/orchestrator/orchestrator.py | 33 ++++++--- src/prime_rl/orchestrator/trajectories.py | 81 +++++++++++++++-------- 2 files changed, 79 insertions(+), 35 deletions(-) diff --git a/src/prime_rl/orchestrator/orchestrator.py b/src/prime_rl/orchestrator/orchestrator.py index 3c051986d4d..e016099c1eb 100644 --- a/src/prime_rl/orchestrator/orchestrator.py +++ b/src/prime_rl/orchestrator/orchestrator.py @@ -480,26 +480,43 @@ async def orchestrate(config: OrchestratorConfig): # Convert rollouts to training samples parallel_preprocess_start = time.perf_counter() - # Pretokenize using the Renderer (same chat template used during rollout) - for rollout in train_rollouts: - pretokenize_rollout_trajectory(rollout, tokenizer, processor=processor, renderer=renderer) + # Stage 1: pretokenize + (for VLM) build image cache concurrently. + # Pretokenize is a no-op when the renderer client already populated + # `tokens` on each trajectory step, but the fallback-tokenizer path + # and image-cache build are both CPU-heavy. Running them on threads + # and awaiting a single gather lets whichever finishes first free + # the event loop immediately and, with max_async_level >= 2, overlaps + # this whole stage with inference for the next batch. + async def _pretokenize_all() -> None: + await asyncio.gather( + *( + asyncio.to_thread( + pretokenize_rollout_trajectory, + rollout, + tokenizer, + processor=processor, + renderer=renderer, + ) + for rollout in train_rollouts + ) + ) - # VLM: build image cache in a thread so it doesn't block the event loop. - # This lets the scheduler continue servicing inflight rollout requests - # and — with max_async_level >= 2 — overlap with the next batch's inference. if is_vlm: - vlm_cache = await asyncio.to_thread(build_vlm_image_cache, train_rollouts, processor) mm_token_type_ids_mapping = {} if hasattr(processor, "image_token_id") and processor.image_token_id is not None: mm_token_type_ids_mapping[processor.image_token_id] = 1 if hasattr(processor, "video_token_id") and processor.video_token_id is not None: mm_token_type_ids_mapping[processor.video_token_id] = 2 - + _, vlm_cache = await asyncio.gather( + _pretokenize_all(), + asyncio.to_thread(build_vlm_image_cache, train_rollouts, processor), + ) logger.info( f"VLM timing: extract={vlm_cache.extract_time:.2f}s, preprocess={vlm_cache.preprocess_time:.2f}s " f"({vlm_cache.num_unique_images} unique images from {vlm_cache.num_unique_examples} examples)" ) else: + await _pretokenize_all() vlm_cache = None mm_token_type_ids_mapping = None diff --git a/src/prime_rl/orchestrator/trajectories.py b/src/prime_rl/orchestrator/trajectories.py index 4bbc8df4ced..3ceccebd57e 100644 --- a/src/prime_rl/orchestrator/trajectories.py +++ b/src/prime_rl/orchestrator/trajectories.py @@ -251,44 +251,51 @@ def pretokenize_rollout_trajectory( def _expand_vlm_image_placeholders( sample: TrainingSample, - image_grid_thw: list[list[int]], + tokens_per_image: list[int], *, image_token_id: int, - merge_size: int, temperature: float, ) -> None: - """In place: replace each un-expanded `<|image_pad|>` in a TrainingSample - with N copies matching the per-image patch count. - - The Qwen3-VL renderer emits one un-expanded placeholder per image so vLLM - can expand them server-side against ``multi_modal_data``. The trainer - forward pass, by contrast, expects placeholders pre-expanded 1:1 with - image features. Every parallel per-token array on the sample (mask, - logprobs, temperatures, routed_experts) has to be padded at the exact + """In place: replace each un-expanded image placeholder in a TrainingSample + with N copies, one per soft image feature the trainer's vision tower will + produce. + + VLM renderers emit a single placeholder per image so vLLM can expand them + server-side against ``multi_modal_data``. The trainer forward expects them + pre-expanded 1:1 with vision features, otherwise it aborts with + "image features and image tokens do not match". Every parallel per-token + array on the sample (mask, logprobs, temperatures) has to be padded at the same positions or the training loss lines up with the wrong tokens. - Idempotent: if the sample already carries expanded placeholders (total - pad count already matches the feature count) we return immediately. Any - other count triggers a hard ``ValueError`` rather than silent drift. + ``tokens_per_image`` is the model-agnostic spec — one integer per image + giving the soft-token count the vision tower produces. For Qwen3-VL that's + ``(t*h*w) // merge_size**2`` derived from ``image_grid_thw``; for + Gemma-style processors it comes back directly as + ``num_soft_tokens_per_image``. The caller computes this list from whatever + metadata the processor exposed; this helper only knows how to splice it. + + Idempotent: if the sample already carries expanded placeholders (total pad + count matches the expected feature count) we return immediately. Any other + count triggers a hard ``ValueError`` rather than silent drift. """ - denom = merge_size * merge_size - expected_features = sum(max(t * h * w // denom, 1) for t, h, w in image_grid_thw) - expected_images = len(image_grid_thw) + expected_features = sum(max(n, 1) for n in tokens_per_image) + expected_images = len(tokens_per_image) total_pads = sample.prompt_ids.count(image_token_id) + sample.completion_ids.count(image_token_id) if total_pads == 0 or total_pads == expected_features: return if total_pads != expected_images: - raise ValueError(f"Expected {expected_images} image placeholder(s) to match image_grid_thw, found {total_pads}") + raise ValueError( + f"Expected {expected_images} image placeholder(s) to match tokens_per_image, found {total_pads}" + ) - grid_iter = iter(image_grid_thw) + counts_iter = iter(tokens_per_image) # Prompt stream: only ids and prompt_mask are per-token. new_prompt_ids: list[int] = [] new_prompt_mask: list[bool] = [] for tok, masked in zip(sample.prompt_ids, sample.prompt_mask, strict=True): if tok == image_token_id: - t, h, w = next(grid_iter) - count = max(t * h * w // denom, 1) + count = max(next(counts_iter), 1) new_prompt_ids.extend([image_token_id] * count) new_prompt_mask.extend([masked] * count) else: @@ -310,8 +317,7 @@ def _expand_vlm_image_placeholders( strict=True, ): if tok == image_token_id: - t, h, w = next(grid_iter) - count = max(t * h * w // denom, 1) + count = max(next(counts_iter), 1) new_comp_ids.extend([image_token_id] * count) new_comp_mask.extend([False] * count) new_comp_lp.extend([0.0] * count) @@ -328,8 +334,8 @@ def _expand_vlm_image_placeholders( sample.completion_mask = new_comp_mask sample.completion_logprobs = new_comp_lp sample.completion_temperatures = new_comp_temps - # VLM rollouts don't ship routed-experts data (dense Qwen3-VL); re-align - # if some future path does provide it so downstream shape checks hold. + # Dense VLMs (Qwen3-VL, Gemma4) don't ship routed-experts data; re-align if + # some future MoE-VLM path does provide it so downstream shape checks hold. if sample.routed_experts is not None: sample.routed_experts = _align_routed_experts( sample.routed_experts, @@ -337,6 +343,16 @@ def _expand_vlm_image_placeholders( ) +def _tokens_per_image_from_grid_thw(grids: list[list[int]], merge_size: int) -> list[int]: + """Qwen2/3-VL-style derivation: patches per image = t*h*w, soft tokens = + patches / merge_size**2. Gemma-style processors bypass this path — their + image_processor already returns ``num_soft_tokens_per_image`` directly, + which we pass through unchanged. + """ + denom = merge_size * merge_size + return [max(t * h * w // denom, 1) for t, h, w in grids] + + def interleave_rollout( output: vf.RolloutOutput, vlm_cache: "VLMImageCache | None" = None, @@ -507,12 +523,23 @@ def extend_sample(sample: TrainingSample, prefix_len: int, step_idx: int) -> Non sample.pixel_values = pv sample.pixel_values_shape = shape sample.image_grid_thw = grids - if grids and image_token_id is not None and merge_size: + # Derive a model-agnostic soft-token count per image. Qwen2/3-VL + # exposes patch geometry via image_grid_thw; Gemma-family processors + # return num_soft_tokens_per_image directly and don't populate grids + # at all. Whichever is present is enough to expand placeholders. + tokens_per_image = None + if grids and image_token_id is not None: + if merge_size: + tokens_per_image = _tokens_per_image_from_grid_thw(grids, merge_size) + else: + # Treat grids as an already-normalized list of per-image + # counts (e.g. [[n], ...]) emitted by a non-Qwen processor. + tokens_per_image = [max(int(g[0]), 1) for g in grids] + if tokens_per_image is not None and image_token_id is not None: _expand_vlm_image_placeholders( sample, - grids, + tokens_per_image, image_token_id=image_token_id, - merge_size=merge_size, temperature=temperature, ) if mm_token_type_ids_mapping is not None: From 03865c602a84c535ca63ed53397cf27251dec957 Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 21 Apr 2026 15:58:24 +0000 Subject: [PATCH 21/54] feat(vlm): route VLMs to MITO, keep renderers for plain LMs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VLM renderer path required expanding image_pad placeholders, threading grid metadata through the orchestrator, and keeping parallel arrays (mask/logprobs/temperatures) aligned — the complexity was never buying anything we couldn't get from server-side chat templating. Route policy in setup_rollout_inference_pool: - VLM -> openai_chat_completions (MITO), renderer=None - plain LM -> renderer client (TITO via /v1/generate) With MITO the orchestrator's fallback _tokenize_step_from_messages path runs apply_chat_template on the processor, which emits fully expanded image_pad ids directly. That makes _expand_vlm_image_placeholders dead code on every live path, so drop it along with _tokens_per_image_from_grid_thw. Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- src/prime_rl/orchestrator/orchestrator.py | 21 +++- src/prime_rl/orchestrator/trajectories.py | 133 +--------------------- uv.lock | 6 +- 4 files changed, 29 insertions(+), 133 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e0ffd3332a2..0e790a1a7f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "2d8cc29" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "7f44237" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] diff --git a/src/prime_rl/orchestrator/orchestrator.py b/src/prime_rl/orchestrator/orchestrator.py index e016099c1eb..7873aefbd68 100644 --- a/src/prime_rl/orchestrator/orchestrator.py +++ b/src/prime_rl/orchestrator/orchestrator.py @@ -152,6 +152,7 @@ async def orchestrate(config: OrchestratorConfig): rollout_client_config=rollout_client_config, rollout_model_name=rollout_model_name, tokenizer=tokenizer, + is_vlm=is_vlm, logger=logger, ) @@ -872,9 +873,17 @@ async def setup_rollout_inference_pool( rollout_client_config, rollout_model_name: str, tokenizer, + is_vlm: bool, logger, ): - """Set up rollout inference for either direct renderer clients or external hard distillation.""" + """Set up rollout inference. + + Routing policy: + - external teacher rollout → MITO (openai_chat_completions), no renderer + - VLM → MITO (openai_chat_completions), no renderer. Image preprocessing + and chat templating live server-side; the client never tokenizes images. + - plain LM → renderer client (TITO via /v1/generate). + """ if config.teacher_rollout_model is not None: logger.info("Using external rollout model without renderer client") inference_pool = await setup_inference_pool( @@ -885,6 +894,16 @@ async def setup_rollout_inference_pool( ) return None, inference_pool + if is_vlm: + logger.info("VLM detected — using MITO (openai_chat_completions) rollout client") + inference_pool = await setup_inference_pool( + rollout_client_config, + model_name=rollout_model_name, + train_client_type="openai_chat_completions", + eval_client_type="openai_chat_completions", + ) + return None, inference_pool + renderer = create_renderer(tokenizer, renderer=config.model.renderer) logger.info(f"Initialized {type(renderer).__name__} for {config.model.name}") inference_pool = await setup_inference_pool( diff --git a/src/prime_rl/orchestrator/trajectories.py b/src/prime_rl/orchestrator/trajectories.py index 3ceccebd57e..a9d86a3cb41 100644 --- a/src/prime_rl/orchestrator/trajectories.py +++ b/src/prime_rl/orchestrator/trajectories.py @@ -249,110 +249,6 @@ def pretokenize_rollout_trajectory( return True -def _expand_vlm_image_placeholders( - sample: TrainingSample, - tokens_per_image: list[int], - *, - image_token_id: int, - temperature: float, -) -> None: - """In place: replace each un-expanded image placeholder in a TrainingSample - with N copies, one per soft image feature the trainer's vision tower will - produce. - - VLM renderers emit a single placeholder per image so vLLM can expand them - server-side against ``multi_modal_data``. The trainer forward expects them - pre-expanded 1:1 with vision features, otherwise it aborts with - "image features and image tokens do not match". Every parallel per-token - array on the sample (mask, logprobs, temperatures) has to be padded at the - same positions or the training loss lines up with the wrong tokens. - - ``tokens_per_image`` is the model-agnostic spec — one integer per image - giving the soft-token count the vision tower produces. For Qwen3-VL that's - ``(t*h*w) // merge_size**2`` derived from ``image_grid_thw``; for - Gemma-style processors it comes back directly as - ``num_soft_tokens_per_image``. The caller computes this list from whatever - metadata the processor exposed; this helper only knows how to splice it. - - Idempotent: if the sample already carries expanded placeholders (total pad - count matches the expected feature count) we return immediately. Any other - count triggers a hard ``ValueError`` rather than silent drift. - """ - expected_features = sum(max(n, 1) for n in tokens_per_image) - expected_images = len(tokens_per_image) - total_pads = sample.prompt_ids.count(image_token_id) + sample.completion_ids.count(image_token_id) - if total_pads == 0 or total_pads == expected_features: - return - if total_pads != expected_images: - raise ValueError( - f"Expected {expected_images} image placeholder(s) to match tokens_per_image, found {total_pads}" - ) - - counts_iter = iter(tokens_per_image) - - # Prompt stream: only ids and prompt_mask are per-token. - new_prompt_ids: list[int] = [] - new_prompt_mask: list[bool] = [] - for tok, masked in zip(sample.prompt_ids, sample.prompt_mask, strict=True): - if tok == image_token_id: - count = max(next(counts_iter), 1) - new_prompt_ids.extend([image_token_id] * count) - new_prompt_mask.extend([masked] * count) - else: - new_prompt_ids.append(tok) - new_prompt_mask.append(masked) - - # Completion stream: ids + mask + logprobs + temperatures stay in lockstep. - # Inserted pad positions are not gradient-tracked and carry no real - # logprob; the extension path already uses mask=False / lp=0.0 for them. - new_comp_ids: list[int] = [] - new_comp_mask: list[bool] = [] - new_comp_lp: list[float] = [] - new_comp_temps: list[float] = [] - for tok, masked, lp, temp in zip( - sample.completion_ids, - sample.completion_mask, - sample.completion_logprobs, - sample.completion_temperatures, - strict=True, - ): - if tok == image_token_id: - count = max(next(counts_iter), 1) - new_comp_ids.extend([image_token_id] * count) - new_comp_mask.extend([False] * count) - new_comp_lp.extend([0.0] * count) - new_comp_temps.extend([temperature] * count) - else: - new_comp_ids.append(tok) - new_comp_mask.append(masked) - new_comp_lp.append(lp) - new_comp_temps.append(temp) - - sample.prompt_ids = new_prompt_ids - sample.prompt_mask = new_prompt_mask - sample.completion_ids = new_comp_ids - sample.completion_mask = new_comp_mask - sample.completion_logprobs = new_comp_lp - sample.completion_temperatures = new_comp_temps - # Dense VLMs (Qwen3-VL, Gemma4) don't ship routed-experts data; re-align if - # some future MoE-VLM path does provide it so downstream shape checks hold. - if sample.routed_experts is not None: - sample.routed_experts = _align_routed_experts( - sample.routed_experts, - len(sample.prompt_ids) + len(sample.completion_ids), - ) - - -def _tokens_per_image_from_grid_thw(grids: list[list[int]], merge_size: int) -> list[int]: - """Qwen2/3-VL-style derivation: patches per image = t*h*w, soft tokens = - patches / merge_size**2. Gemma-style processors bypass this path — their - image_processor already returns ``num_soft_tokens_per_image`` directly, - which we pass through unchanged. - """ - denom = merge_size * merge_size - return [max(t * h * w // denom, 1) for t, h, w in grids] - - def interleave_rollout( output: vf.RolloutOutput, vlm_cache: "VLMImageCache | None" = None, @@ -511,37 +407,18 @@ def extend_sample(sample: TrainingSample, prefix_len: int, step_idx: int) -> Non new_prefix = tokens["prompt_ids"] + tokens["completion_ids"] active_samples.append((new_prefix, make_sample(tokens), step_idx)) - # Attach images once per sample using only the last merged step + # Attach images once per sample using only the last merged step. Prompt + # tokens already contain fully expanded <|image_pad|> placeholders because + # VLMs go through MITO (chat completions), which runs apply_chat_template + # server-side; the orchestrator re-tokenizes via the same processor in the + # fallback path so features and tokens stay 1:1. if vlm_cache is not None: key = output["example_id"] if cache_key is None else cache_key - image_token_id = getattr(processor, "image_token_id", None) if processor is not None else None - merge_size = ( - getattr(getattr(processor, "image_processor", None), "merge_size", None) if processor is not None else None - ) for _, sample, last_step_idx in active_samples: pv, shape, grids = vlm_cache.get_for_step(key, last_step_idx) sample.pixel_values = pv sample.pixel_values_shape = shape sample.image_grid_thw = grids - # Derive a model-agnostic soft-token count per image. Qwen2/3-VL - # exposes patch geometry via image_grid_thw; Gemma-family processors - # return num_soft_tokens_per_image directly and don't populate grids - # at all. Whichever is present is enough to expand placeholders. - tokens_per_image = None - if grids and image_token_id is not None: - if merge_size: - tokens_per_image = _tokens_per_image_from_grid_thw(grids, merge_size) - else: - # Treat grids as an already-normalized list of per-image - # counts (e.g. [[n], ...]) emitted by a non-Qwen processor. - tokens_per_image = [max(int(g[0]), 1) for g in grids] - if tokens_per_image is not None and image_token_id is not None: - _expand_vlm_image_placeholders( - sample, - tokens_per_image, - image_token_id=image_token_id, - temperature=temperature, - ) if mm_token_type_ids_mapping is not None: sample.mm_token_type_ids = [ mm_token_type_ids_mapping.get(token_id, 0) for token_id in sample.prompt_ids + sample.completion_ids diff --git a/uv.lock b/uv.lock index c3324d075df..13e92146922 100644 --- a/uv.lock +++ b/uv.lock @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=2d8cc29" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=7f44237" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3249,7 +3249,7 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=2d8cc29#2d8cc292b65ee447064e827ae2bfeaa3d822e250" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=7f44237#7f44237783f345db162e65e0921a59a193e967e7" } dependencies = [ { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] @@ -4044,7 +4044,7 @@ wheels = [ [[package]] name = "verifiers" version = "0.1.12.dev5" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=2d8cc29#2d8cc292b65ee447064e827ae2bfeaa3d822e250" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=7f44237#7f44237783f345db162e65e0921a59a193e967e7" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From fd9d0f54ea5b84fac9de52165cdd89b25d0199ee Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 21 Apr 2026 16:37:25 +0000 Subject: [PATCH 22/54] chore(orchestrator): bump verifiers to pool-size-1 default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pairs with verifiers d420651. RendererPool default is now 1, so the orchestrator doesn't need an eager pre-warm — first rollout builds a single tokenizer in ~0.3s instead of stalling on 16. Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0e790a1a7f7..debdd319fdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "7f44237" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "d420651" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] diff --git a/uv.lock b/uv.lock index 13e92146922..2e74f8c4b1b 100644 --- a/uv.lock +++ b/uv.lock @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=7f44237" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=d420651" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3249,7 +3249,7 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=7f44237#7f44237783f345db162e65e0921a59a193e967e7" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=d420651#d4206510c1f11b939694d31c8b91f8f8c9aa784e" } dependencies = [ { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] @@ -4044,7 +4044,7 @@ wheels = [ [[package]] name = "verifiers" version = "0.1.12.dev5" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=7f44237#7f44237783f345db162e65e0921a59a193e967e7" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=d420651#d4206510c1f11b939694d31c8b91f8f8c9aa784e" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From ca3dcdde51f773315628af07dc927d336145f800 Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 21 Apr 2026 16:42:20 +0000 Subject: [PATCH 23/54] perf(serving_generate): stop polling is_disconnected per decode step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On /v1/generate we were awaiting raw_request.is_disconnected() once per yielded token. With ~2k concurrent rollouts each producing dozens of tokens, that's 100k+ extra ASGI receive() awaits per orchestrator step, and it was holding requests in the scheduler — vLLM's num_requests_running averaged 10+ on renderers vs 0.3 on main, with prefill throughput ~3% behind and completed_requests_per_s ~2.5% behind. Switch to the CancelledError pattern vLLM's own chat_completions uses: Starlette cancels the coroutine on client disconnect, so we abort in the except branch instead of polling. Measured on reverse-text (20 steps, 2k rollouts/step, Qwen3-0.6B): this single change dropped orchestrator wall from 55.5s to 52.6s (main is 52s), num_requests_running from 10.17 to 1.64, and lifted prefill throughput past main (3156 vs 3082 tps). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../inference/vllm/serving_generate.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/prime_rl/inference/vllm/serving_generate.py b/src/prime_rl/inference/vllm/serving_generate.py index e9581fb4cb1..e620bee7f4b 100644 --- a/src/prime_rl/inference/vllm/serving_generate.py +++ b/src/prime_rl/inference/vllm/serving_generate.py @@ -10,6 +10,7 @@ from __future__ import annotations +import asyncio import base64 from collections.abc import Mapping from io import BytesIO @@ -130,14 +131,22 @@ async def generate(self, request: GenerateRequest, raw_request: Request) -> Gene data_parallel_rank=data_parallel_rank, ) - async for output in generator: - if await raw_request.is_disconnected(): - await self.engine_client.abort(request_id) - return {"error": "Client disconnected"} - for comp_output in output.outputs: - if comp_output.routed_experts is not None: - routed_experts_map[comp_output.index] = _encode_routed_experts(comp_output.routed_experts) - final_output = output + # Drain the generator without polling ``raw_request.is_disconnected`` + # per decode step. That poll is one ASGI ``receive()`` await per + # yielded token — at ~2k concurrent rollouts each producing dozens of + # tokens, it was the single largest per-step overhead on the /generate + # path (vLLM's own chat completions handler uses the CancelledError + # pattern for the same reason). Starlette cancels this coroutine on + # client disconnect, so the except branch still catches it. + try: + async for output in generator: + for comp_output in output.outputs: + if comp_output.routed_experts is not None: + routed_experts_map[comp_output.index] = _encode_routed_experts(comp_output.routed_experts) + final_output = output + except asyncio.CancelledError: + await self.engine_client.abort(request_id) + raise if final_output is None: return {"error": "No output generated"} From 5093054217ac95cf766403775ee48228f15dbd69 Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 21 Apr 2026 18:53:47 +0000 Subject: [PATCH 24/54] fix(serving_generate): default max_tokens to context room, not 4096 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hard-coded max_tokens=4096 silently capped long completions on 8k+ models. Observed on hendrycks-math-sanity with DeepSeek-R1-Distill-Qwen-1.5B: 82% of rendered rollouts hit 4096 truncation (avg completion 3914 tok) vs main's chat_completions path which defaults to max_model_len-prompt_len and averaged 6209 tok — and the renderer-path average reward was 0.244 vs 0.551 on main because reasoning got chopped mid-thought. Match /v1/chat/completions: when the client doesn't specify a cap, fill it with (max_model_len - len(prompt_token_ids)). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/prime_rl/inference/vllm/serving_generate.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/prime_rl/inference/vllm/serving_generate.py b/src/prime_rl/inference/vllm/serving_generate.py index e620bee7f4b..58696db6448 100644 --- a/src/prime_rl/inference/vllm/serving_generate.py +++ b/src/prime_rl/inference/vllm/serving_generate.py @@ -44,7 +44,12 @@ class GenerateRequest(BaseModel): prompt_token_ids: list[int] images: list[RawImageData] | None = None - max_tokens: int = 4096 + # When unset, fill from max_model_len - prompt_len at request time so we + # match /v1/chat/completions behavior. The previous 4096 hard default + # silently truncated long completions on 8k+ context runs (e.g. hendrycks + # reasoning rollouts capped at 4096 tokens, making rendered rollouts look + # shorter than main's for the same model). + max_tokens: int | None = None temperature: float = 1.0 top_p: float = 1.0 top_k: int = -1 @@ -94,8 +99,15 @@ async def generate(self, request: GenerateRequest, raw_request: Request) -> Gene pil_images = [Image.open(BytesIO(base64.b64decode(img.data))) for img in request.images] engine_prompt["multi_modal_data"] = {"image": pil_images} + # Match /v1/chat/completions: if the client didn't ask for a specific + # cap, let the model generate up to whatever room is left in context. + max_tokens = request.max_tokens + if max_tokens is None: + model_config = await self.engine_client.get_model_config() + max_tokens = max(1, model_config.max_model_len - len(request.prompt_token_ids)) + sampling_params = SamplingParams( - max_tokens=request.max_tokens, + max_tokens=max_tokens, temperature=request.temperature, top_p=request.top_p, top_k=request.top_k, From 9cf49b0b041c03ffcc14bee0d339b51a42146939 Mon Sep 17 00:00:00 2001 From: hallerite Date: Tue, 21 Apr 2026 21:22:15 +0000 Subject: [PATCH 25/54] fix(serving_generate): use engine model_config attr, not async getter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AsyncLLM has no get_model_config() async method on v1 — the attribute access self.engine_client.model_config.max_model_len works directly. The broken variant 500'd on every rollout. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/prime_rl/inference/vllm/serving_generate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/prime_rl/inference/vllm/serving_generate.py b/src/prime_rl/inference/vllm/serving_generate.py index 58696db6448..3a209e5bb4b 100644 --- a/src/prime_rl/inference/vllm/serving_generate.py +++ b/src/prime_rl/inference/vllm/serving_generate.py @@ -101,10 +101,11 @@ async def generate(self, request: GenerateRequest, raw_request: Request) -> Gene # Match /v1/chat/completions: if the client didn't ask for a specific # cap, let the model generate up to whatever room is left in context. + # vLLM v1 AsyncLLM exposes model_config directly (no async getter). max_tokens = request.max_tokens if max_tokens is None: - model_config = await self.engine_client.get_model_config() - max_tokens = max(1, model_config.max_model_len - len(request.prompt_token_ids)) + max_model_len = self.engine_client.model_config.max_model_len + max_tokens = max(1, max_model_len - len(request.prompt_token_ids)) sampling_params = SamplingParams( max_tokens=max_tokens, From b1843b4785da561bbf0537e26bd9eb909212fb78 Mon Sep 17 00:00:00 2001 From: hallerite Date: Wed, 22 Apr 2026 18:33:24 +0530 Subject: [PATCH 26/54] refactor(renderer): drop VLM residue from renderer path Three related changes that reflect the final routing policy: VLMs go through MITO (/v1/chat/completions, server-side templating), plain LMs go through the renderer client (/v1/generate, client-side tokens). - orchestrator.setup_rollout_inference_pool: raise if model.vlm is set alongside an explicit renderer choice. Leaving renderer='auto' still silently routes to MITO (correct). This turns a silent contradiction into a loud configuration error. - inference/vllm/serving_generate.py: the /v1/generate endpoint is text-only. Drops the RawImageData schema, the Image/BytesIO/base64 decode path, and the multi_modal_data branch. Any image traffic now either goes through /v1/chat/completions or fails loud at the schema layer. - pyproject: bump verifiers pin to 939a633, which contains the matching client-side image removal and the DefaultRenderer auto-detect warning. Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- .../inference/vllm/serving_generate.py | 30 +++++-------------- src/prime_rl/orchestrator/orchestrator.py | 6 ++++ 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index debdd319fdd..24e25c7ef9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "d420651" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "939a633" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] diff --git a/src/prime_rl/inference/vllm/serving_generate.py b/src/prime_rl/inference/vllm/serving_generate.py index 3a209e5bb4b..8d5108293a5 100644 --- a/src/prime_rl/inference/vllm/serving_generate.py +++ b/src/prime_rl/inference/vllm/serving_generate.py @@ -1,11 +1,11 @@ -"""/v1/generate endpoint — accepts pre-tokenized + pre-processed inputs. - -Text-only: accepts prompt_token_ids, passes to engine. -VLM: accepts prompt_token_ids + raw images (multi_modal_data), vLLM processes -images and expands placeholders. The Renderer does all text tokenization -client-side — vLLM just handles image processing and generation. +"""/v1/generate endpoint — accepts pre-tokenized inputs. +Text-only tokens in, tokens out. The Renderer does all tokenization client-side. No Jinja rendering, no server-side chat template application. + +VLMs do not use this endpoint. The orchestrator routes VLMs to MITO +(/v1/chat/completions) where vLLM handles image preprocessing and chat +templating server-side. """ from __future__ import annotations @@ -13,14 +13,12 @@ import asyncio import base64 from collections.abc import Mapping -from io import BytesIO from typing import Any from uuid import uuid4 import numpy as np from fastapi import Request -from PIL import Image -from pydantic import BaseModel, Field +from pydantic import BaseModel from vllm.engine.protocol import EngineClient from vllm.logger import init_logger from vllm.outputs import RequestOutput @@ -32,17 +30,9 @@ # ── Request / Response schemas ─────────────────────────────────────── -class RawImageData(BaseModel): - """Raw image bytes — vLLM processes them server-side.""" - - data: str = Field(description="Base64-encoded image bytes") - media_type: str = Field(default="image/png") - - class GenerateRequest(BaseModel): model: str | None = None prompt_token_ids: list[int] - images: list[RawImageData] | None = None # When unset, fill from max_model_len - prompt_len at request time so we # match /v1/chat/completions behavior. The previous 4096 hard default @@ -84,7 +74,7 @@ class GenerateResponse(BaseModel): class OpenAIServingGenerate: - """Lightweight generate handler — tokens + optional images in, tokens out.""" + """Lightweight generate handler — tokens in, tokens out.""" def __init__(self, engine_client: EngineClient, chat_handler: Any | None = None): self.engine_client = engine_client @@ -95,10 +85,6 @@ async def generate(self, request: GenerateRequest, raw_request: Request) -> Gene "prompt_token_ids": request.prompt_token_ids, } - if request.images: - pil_images = [Image.open(BytesIO(base64.b64decode(img.data))) for img in request.images] - engine_prompt["multi_modal_data"] = {"image": pil_images} - # Match /v1/chat/completions: if the client didn't ask for a specific # cap, let the model generate up to whatever room is left in context. # vLLM v1 AsyncLLM exposes model_config directly (no async getter). diff --git a/src/prime_rl/orchestrator/orchestrator.py b/src/prime_rl/orchestrator/orchestrator.py index 7873aefbd68..1aa650bf37c 100644 --- a/src/prime_rl/orchestrator/orchestrator.py +++ b/src/prime_rl/orchestrator/orchestrator.py @@ -895,6 +895,12 @@ async def setup_rollout_inference_pool( return None, inference_pool if is_vlm: + if config.model.renderer != "auto": + raise ValueError( + f"VLM models must use MITO (server-side chat templating), " + f"but model.renderer={config.model.renderer!r} was set explicitly. " + f"Remove the renderer field or leave it as 'auto' for VLMs." + ) logger.info("VLM detected — using MITO (openai_chat_completions) rollout client") inference_pool = await setup_inference_pool( rollout_client_config, From d54ff3016d42b7dd630c2ddd2b21c0ee800ba160 Mon Sep 17 00:00:00 2001 From: hallerite Date: Wed, 22 Apr 2026 18:38:14 +0530 Subject: [PATCH 27/54] fix(serving_generate): use TokensInput shape to silence vLLM deprecation vLLM 0.19's InputProcessor emits a warning per request if you pass a raw prompt (list[int] / str) instead of a rendered TypedDict with a "type" key. At our rollout volumes the warning floods the inference log; it was also a removal target in the vLLM roadmap, so we'd rather migrate now than chase a hard break later. Uses vllm.inputs.engine.tokens_input() to wrap the prompt token ids as a TokensInput ({"type": "token", "prompt_token_ids": [...]}), which is what vllm/v1/engine/input_processor.py:222 matches on to skip the warning branch. Test updated to assert the new shape. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/prime_rl/inference/vllm/serving_generate.py | 7 ++++--- tests/unit/inference/test_serving_generate.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/prime_rl/inference/vllm/serving_generate.py b/src/prime_rl/inference/vllm/serving_generate.py index 8d5108293a5..91f20a28568 100644 --- a/src/prime_rl/inference/vllm/serving_generate.py +++ b/src/prime_rl/inference/vllm/serving_generate.py @@ -20,6 +20,7 @@ from fastapi import Request from pydantic import BaseModel from vllm.engine.protocol import EngineClient +from vllm.inputs.engine import tokens_input from vllm.logger import init_logger from vllm.outputs import RequestOutput from vllm.sampling_params import SamplingParams @@ -81,9 +82,9 @@ def __init__(self, engine_client: EngineClient, chat_handler: Any | None = None) self.chat_handler = chat_handler async def generate(self, request: GenerateRequest, raw_request: Request) -> GenerateResponse | dict: - engine_prompt: dict[str, Any] = { - "prompt_token_ids": request.prompt_token_ids, - } + # Pre-rendered TokensInput shape (type="token") — avoids vLLM's + # "raw prompt" deprecation that targets plain lists/strings. + engine_prompt = tokens_input(request.prompt_token_ids) # Match /v1/chat/completions: if the client didn't ask for a specific # cap, let the model generate up to whatever room is left in context. diff --git a/tests/unit/inference/test_serving_generate.py b/tests/unit/inference/test_serving_generate.py index f53641186d5..576d5352ea9 100644 --- a/tests/unit/inference/test_serving_generate.py +++ b/tests/unit/inference/test_serving_generate.py @@ -83,7 +83,7 @@ async def _run(): assert response.choices[0].logprobs == [-0.6] call = engine_client.calls[0] - assert call["engine_prompt"] == {"prompt_token_ids": [10, 11, 12]} + assert call["engine_prompt"] == {"type": "token", "prompt_token_ids": [10, 11, 12]} assert call["sampling_params"].prompt_logprobs == 1 assert call["kwargs"]["lora_request"] is handler.chat_handler.lora_request assert call["kwargs"]["priority"] == 7 From 05a35166633f4b94ea63f0cfb3fe1a7ffe425b70 Mon Sep 17 00:00:00 2001 From: hallerite Date: Wed, 22 Apr 2026 19:23:06 +0530 Subject: [PATCH 28/54] feat(config): pluggable tool_parser / reasoning_parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Threads verifiers' new parser registry knobs through the RL config and inference pool plumbing. - BaseModelConfig (orchestrator/trainer/inference model sub-configs) gains optional tool_parser and reasoning_parser string fields. - SharedModelConfig (top-level [model]) gains the same fields with a cascade rule in auto_setup_model: top-level [model].tool_parser propagates to trainer.model, orchestrator.model, and inference.model, same pattern as [model].name and [model].vlm. - setup_inference_pool, StaticInferencePool, ElasticInferencePool, and setup_clients all accept and forward tool_parser / reasoning_parser into ClientConfig. The verifiers RendererClient keys its shared pool cache on (model, renderer, tool_parser, reasoning_parser) so distinct parser configs for the same model don't alias. - orchestrator.setup_rollout_inference_pool passes both fields to create_renderer(...) for the orchestrator-side renderer instance. - Bump verifiers pin to 1d308d2 (the commit that ships the parsers module and DefaultRenderer integration). Usage: uv run rl @ config.toml --model.tool-parser qwen3 --model.reasoning-parser think The cascade puts the same values in all three sub-configs. Parsers are only consumed by DefaultRenderer — for model-specific renderers they are accepted but ignored with an info log. Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- src/prime_rl/configs/rl.py | 22 ++++++++++++++++++++++ src/prime_rl/configs/shared.py | 21 +++++++++++++++++++++ src/prime_rl/orchestrator/orchestrator.py | 9 ++++++++- src/prime_rl/utils/client.py | 14 ++++++++++++++ src/prime_rl/utils/elastic.py | 10 ++++++++++ uv.lock | 6 +++--- 7 files changed, 79 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 24e25c7ef9c..6cdbfaed26a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "939a633" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "1d308d2" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] diff --git a/src/prime_rl/configs/rl.py b/src/prime_rl/configs/rl.py index 3b763e83877..dd9ccf777ca 100644 --- a/src/prime_rl/configs/rl.py +++ b/src/prime_rl/configs/rl.py @@ -142,6 +142,16 @@ class SharedModelConfig(BaseConfig): Field(description="VLM configuration. Set to enable vision-language model support."), ] = None + tool_parser: Annotated[ + str | None, + Field(description="Tool parser name (propagates to orchestrator/trainer/inference model configs)."), + ] = None + + reasoning_parser: Annotated[ + str | None, + Field(description="Reasoning parser name (propagates to orchestrator/trainer/inference model configs)."), + ] = None + class SharedWeightBroadcastConfig(BaseConfig): """Configures shared weight broadcast settings.""" @@ -557,6 +567,18 @@ def auto_setup_model(self): if self.inference is not None: self.inference.model.vlm = self.model.vlm + if self.model.tool_parser is not None: + self.trainer.model.tool_parser = self.model.tool_parser + self.orchestrator.model.tool_parser = self.model.tool_parser + if self.inference is not None: + self.inference.model.tool_parser = self.model.tool_parser + + if self.model.reasoning_parser is not None: + self.trainer.model.reasoning_parser = self.model.reasoning_parser + self.orchestrator.model.reasoning_parser = self.model.reasoning_parser + if self.inference is not None: + self.inference.model.reasoning_parser = self.model.reasoning_parser + validate_shared_model_name(self.trainer, self.orchestrator, self.inference) return self diff --git a/src/prime_rl/configs/shared.py b/src/prime_rl/configs/shared.py index efcbcd50f63..119dd8bf8ce 100644 --- a/src/prime_rl/configs/shared.py +++ b/src/prime_rl/configs/shared.py @@ -136,6 +136,27 @@ class BaseModelConfig(BaseConfig): ), ] = "auto" + tool_parser: Annotated[ + str | None, + Field( + description=( + "Name of a tool parser in renderers.parsers. Only consumed by " + "DefaultRenderer (model-specific renderers bake their own parsing " + "in). Options today: 'qwen3', 'qwen3.5', 'glm', 'deepseek_v3'." + ), + ), + ] = None + + reasoning_parser: Annotated[ + str | None, + Field( + description=( + "Name of a reasoning parser in renderers.parsers. Only consumed " + "by DefaultRenderer. Options today: 'think'." + ), + ), + ] = None + vlm: Annotated[ "VLMConfig | None", Field( diff --git a/src/prime_rl/orchestrator/orchestrator.py b/src/prime_rl/orchestrator/orchestrator.py index 1aa650bf37c..88762d067c2 100644 --- a/src/prime_rl/orchestrator/orchestrator.py +++ b/src/prime_rl/orchestrator/orchestrator.py @@ -910,7 +910,12 @@ async def setup_rollout_inference_pool( ) return None, inference_pool - renderer = create_renderer(tokenizer, renderer=config.model.renderer) + renderer = create_renderer( + tokenizer, + renderer=config.model.renderer, + tool_parser=config.model.tool_parser, + reasoning_parser=config.model.reasoning_parser, + ) logger.info(f"Initialized {type(renderer).__name__} for {config.model.name}") inference_pool = await setup_inference_pool( rollout_client_config, @@ -918,6 +923,8 @@ async def setup_rollout_inference_pool( train_client_type="renderer", eval_client_type="openai_chat_completions", renderer_name=config.model.renderer, + tool_parser=config.model.tool_parser, + reasoning_parser=config.model.reasoning_parser, ) logger.info("Using direct renderer rollout client") return renderer, inference_pool diff --git a/src/prime_rl/utils/client.py b/src/prime_rl/utils/client.py index 739a4de0522..b71e00ed0ad 100644 --- a/src/prime_rl/utils/client.py +++ b/src/prime_rl/utils/client.py @@ -65,6 +65,8 @@ def __init__( train_client_type: str = "renderer", eval_client_type: str = "openai_chat_completions", renderer_name: str = "auto", + tool_parser: str | None = None, + reasoning_parser: str | None = None, ): renderer_model_name = model_name if train_client_type == "renderer" else None self._train_clients = setup_clients( @@ -72,6 +74,8 @@ def __init__( client_type=train_client_type, renderer_name=renderer_name, renderer_model_name=renderer_model_name, + tool_parser=tool_parser, + reasoning_parser=reasoning_parser, ) self._eval_clients = setup_clients(client_config, client_type=eval_client_type) self._admin_clients = setup_admin_clients(client_config) @@ -117,6 +121,8 @@ async def setup_inference_pool( train_client_type: str = "renderer", eval_client_type: str = "openai_chat_completions", renderer_name: str = "auto", + tool_parser: str | None = None, + reasoning_parser: str | None = None, ) -> InferencePool: """Create an inference pool from config (static or elastic).""" logger = get_logger() @@ -137,6 +143,8 @@ async def setup_inference_pool( train_client_type=train_client_type, eval_client_type=eval_client_type, renderer_name=renderer_name, + tool_parser=tool_parser, + reasoning_parser=reasoning_parser, ) logger.info( @@ -150,6 +158,8 @@ async def setup_inference_pool( train_client_type=train_client_type, eval_client_type=eval_client_type, renderer_name=renderer_name, + tool_parser=tool_parser, + reasoning_parser=reasoning_parser, ) @@ -158,6 +168,8 @@ def setup_clients( client_type: str = "openai_chat_completions", renderer_name: str = "auto", renderer_model_name: str | None = None, + tool_parser: str | None = None, + reasoning_parser: str | None = None, ) -> list[vf.ClientConfig]: clients = [] client_idx = 0 @@ -172,6 +184,8 @@ def setup_clients( client_type=client_type, renderer=renderer_name, renderer_model_name=renderer_model_name, + tool_parser=tool_parser, + reasoning_parser=reasoning_parser, api_base_url=base_url, api_key_var=client_config.api_key_var, timeout=client_config.timeout, diff --git a/src/prime_rl/utils/elastic.py b/src/prime_rl/utils/elastic.py index 612d1997cff..b7202516654 100644 --- a/src/prime_rl/utils/elastic.py +++ b/src/prime_rl/utils/elastic.py @@ -107,6 +107,8 @@ def __init__( train_client_type: str = "renderer", eval_client_type: str = "openai_chat_completions", renderer_name: str = "auto", + tool_parser: str | None = None, + reasoning_parser: str | None = None, ): self.logger = get_logger() self.client_config = client_config @@ -119,6 +121,8 @@ def __init__( self.train_client_type = train_client_type self.eval_client_type = eval_client_type self.renderer_name = renderer_name + self.tool_parser = tool_parser + self.reasoning_parser = reasoning_parser self.router_url = client_config.router_url self._servers: dict[str, ServerState] = {} @@ -143,6 +147,8 @@ async def from_config( train_client_type: str = "renderer", eval_client_type: str = "openai_chat_completions", renderer_name: str = "auto", + tool_parser: str | None = None, + reasoning_parser: str | None = None, ) -> ElasticInferencePool: if client_config.elastic is None: raise ValueError("Elastic inference pool requires elastic config") @@ -152,6 +158,8 @@ async def from_config( train_client_type=train_client_type, eval_client_type=eval_client_type, renderer_name=renderer_name, + tool_parser=tool_parser, + reasoning_parser=reasoning_parser, ) await pool.start() return pool @@ -199,6 +207,8 @@ def _rebuild_clients(self) -> None: client_type=self.train_client_type, renderer_name=self.renderer_name, renderer_model_name=self.renderer_model_name, + tool_parser=self.tool_parser, + reasoning_parser=self.reasoning_parser, ) if urls else [] diff --git a/uv.lock b/uv.lock index 2e74f8c4b1b..2a1d575e2f7 100644 --- a/uv.lock +++ b/uv.lock @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=d420651" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=1d308d2" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3249,7 +3249,7 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=d420651#d4206510c1f11b939694d31c8b91f8f8c9aa784e" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=1d308d2#1d308d28da72ee170119c14c155c33f24a25ec56" } dependencies = [ { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] @@ -4044,7 +4044,7 @@ wheels = [ [[package]] name = "verifiers" version = "0.1.12.dev5" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=d420651#d4206510c1f11b939694d31c8b91f8f8c9aa784e" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=1d308d2#1d308d28da72ee170119c14c155c33f24a25ec56" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From 10d3d98b8b21903c3f23c9af0babec288b337eb6 Mon Sep 17 00:00:00 2001 From: hallerite Date: Wed, 22 Apr 2026 19:47:45 +0530 Subject: [PATCH 29/54] feat(config): expose model.renderer_pool_size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wordle parity vs main showed ~15% step-time overhead and a tiny KL drift on the renderer path. Per-rollout vLLM generation_ms was lower on renderers (18s vs 20s), so the gap lives in client-side jinja rendering. With ~1024 concurrent rollouts × 5.8 turns × 2-3 jinja renders per turn (bridge trick + occasional full re-render), the default pool size of 1 (verifiers _DEFAULT_POOL_SIZE) serializes the GIL-bound jinja half through a single renderer slot. - BaseModelConfig and SharedModelConfig gain optional renderer_pool_size: int | None = None. Cascade in auto_setup_model propagates the top-level [model] value to trainer/orchestrator/ inference like the other renderer fields. - setup_inference_pool, StaticInferencePool, ElasticInferencePool, setup_clients all accept and forward the field into vf.ClientConfig (verifiers 0e95edf which keys its shared pool cache on pool size too, so distinct sizes for the same model don't alias). - orchestrator.setup_rollout_inference_pool reads it from config.model.renderer_pool_size. - Bump verifiers pin to 0e95edf. Usage: --model.renderer-pool-size 8 # for long multi-turn workloads Default remains None (verifiers default of 1) — keeps current behavior for short-prompt RL where dispatch overhead dominates parallelism. Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- src/prime_rl/configs/rl.py | 11 +++++++++++ src/prime_rl/configs/shared.py | 12 ++++++++++++ src/prime_rl/orchestrator/orchestrator.py | 1 + src/prime_rl/utils/client.py | 7 +++++++ src/prime_rl/utils/elastic.py | 5 +++++ uv.lock | 6 +++--- 7 files changed, 40 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6cdbfaed26a..e7f5da6d238 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "1d308d2" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "0e95edf" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] diff --git a/src/prime_rl/configs/rl.py b/src/prime_rl/configs/rl.py index dd9ccf777ca..f884973b755 100644 --- a/src/prime_rl/configs/rl.py +++ b/src/prime_rl/configs/rl.py @@ -152,6 +152,11 @@ class SharedModelConfig(BaseConfig): Field(description="Reasoning parser name (propagates to orchestrator/trainer/inference model configs)."), ] = None + renderer_pool_size: Annotated[ + int | None, + Field(ge=1, description="Renderer pool size (propagates to orchestrator/trainer/inference)."), + ] = None + class SharedWeightBroadcastConfig(BaseConfig): """Configures shared weight broadcast settings.""" @@ -579,6 +584,12 @@ def auto_setup_model(self): if self.inference is not None: self.inference.model.reasoning_parser = self.model.reasoning_parser + if self.model.renderer_pool_size is not None: + self.trainer.model.renderer_pool_size = self.model.renderer_pool_size + self.orchestrator.model.renderer_pool_size = self.model.renderer_pool_size + if self.inference is not None: + self.inference.model.renderer_pool_size = self.model.renderer_pool_size + validate_shared_model_name(self.trainer, self.orchestrator, self.inference) return self diff --git a/src/prime_rl/configs/shared.py b/src/prime_rl/configs/shared.py index 119dd8bf8ce..adecc7ebe4d 100644 --- a/src/prime_rl/configs/shared.py +++ b/src/prime_rl/configs/shared.py @@ -157,6 +157,18 @@ class BaseModelConfig(BaseConfig): ), ] = None + renderer_pool_size: Annotated[ + int | None, + Field( + ge=1, + description=( + "Number of renderer slots shared across concurrent rollouts. " + "None keeps the verifiers default (1). Bump for long multi-turn " + "prompts where client-side jinja tokenization serializes." + ), + ), + ] = None + vlm: Annotated[ "VLMConfig | None", Field( diff --git a/src/prime_rl/orchestrator/orchestrator.py b/src/prime_rl/orchestrator/orchestrator.py index 88762d067c2..c59b87ce6e7 100644 --- a/src/prime_rl/orchestrator/orchestrator.py +++ b/src/prime_rl/orchestrator/orchestrator.py @@ -925,6 +925,7 @@ async def setup_rollout_inference_pool( renderer_name=config.model.renderer, tool_parser=config.model.tool_parser, reasoning_parser=config.model.reasoning_parser, + renderer_pool_size=config.model.renderer_pool_size, ) logger.info("Using direct renderer rollout client") return renderer, inference_pool diff --git a/src/prime_rl/utils/client.py b/src/prime_rl/utils/client.py index b71e00ed0ad..53939d72604 100644 --- a/src/prime_rl/utils/client.py +++ b/src/prime_rl/utils/client.py @@ -67,6 +67,7 @@ def __init__( renderer_name: str = "auto", tool_parser: str | None = None, reasoning_parser: str | None = None, + renderer_pool_size: int | None = None, ): renderer_model_name = model_name if train_client_type == "renderer" else None self._train_clients = setup_clients( @@ -76,6 +77,7 @@ def __init__( renderer_model_name=renderer_model_name, tool_parser=tool_parser, reasoning_parser=reasoning_parser, + renderer_pool_size=renderer_pool_size, ) self._eval_clients = setup_clients(client_config, client_type=eval_client_type) self._admin_clients = setup_admin_clients(client_config) @@ -123,6 +125,7 @@ async def setup_inference_pool( renderer_name: str = "auto", tool_parser: str | None = None, reasoning_parser: str | None = None, + renderer_pool_size: int | None = None, ) -> InferencePool: """Create an inference pool from config (static or elastic).""" logger = get_logger() @@ -145,6 +148,7 @@ async def setup_inference_pool( renderer_name=renderer_name, tool_parser=tool_parser, reasoning_parser=reasoning_parser, + renderer_pool_size=renderer_pool_size, ) logger.info( @@ -160,6 +164,7 @@ async def setup_inference_pool( renderer_name=renderer_name, tool_parser=tool_parser, reasoning_parser=reasoning_parser, + renderer_pool_size=renderer_pool_size, ) @@ -170,6 +175,7 @@ def setup_clients( renderer_model_name: str | None = None, tool_parser: str | None = None, reasoning_parser: str | None = None, + renderer_pool_size: int | None = None, ) -> list[vf.ClientConfig]: clients = [] client_idx = 0 @@ -184,6 +190,7 @@ def setup_clients( client_type=client_type, renderer=renderer_name, renderer_model_name=renderer_model_name, + renderer_pool_size=renderer_pool_size, tool_parser=tool_parser, reasoning_parser=reasoning_parser, api_base_url=base_url, diff --git a/src/prime_rl/utils/elastic.py b/src/prime_rl/utils/elastic.py index b7202516654..0e8c2759e06 100644 --- a/src/prime_rl/utils/elastic.py +++ b/src/prime_rl/utils/elastic.py @@ -109,6 +109,7 @@ def __init__( renderer_name: str = "auto", tool_parser: str | None = None, reasoning_parser: str | None = None, + renderer_pool_size: int | None = None, ): self.logger = get_logger() self.client_config = client_config @@ -123,6 +124,7 @@ def __init__( self.renderer_name = renderer_name self.tool_parser = tool_parser self.reasoning_parser = reasoning_parser + self.renderer_pool_size = renderer_pool_size self.router_url = client_config.router_url self._servers: dict[str, ServerState] = {} @@ -149,6 +151,7 @@ async def from_config( renderer_name: str = "auto", tool_parser: str | None = None, reasoning_parser: str | None = None, + renderer_pool_size: int | None = None, ) -> ElasticInferencePool: if client_config.elastic is None: raise ValueError("Elastic inference pool requires elastic config") @@ -160,6 +163,7 @@ async def from_config( renderer_name=renderer_name, tool_parser=tool_parser, reasoning_parser=reasoning_parser, + renderer_pool_size=renderer_pool_size, ) await pool.start() return pool @@ -209,6 +213,7 @@ def _rebuild_clients(self) -> None: renderer_model_name=self.renderer_model_name, tool_parser=self.tool_parser, reasoning_parser=self.reasoning_parser, + renderer_pool_size=self.renderer_pool_size, ) if urls else [] diff --git a/uv.lock b/uv.lock index 2a1d575e2f7..df56997336b 100644 --- a/uv.lock +++ b/uv.lock @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=1d308d2" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=0e95edf" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3249,7 +3249,7 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=1d308d2#1d308d28da72ee170119c14c155c33f24a25ec56" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=0e95edf#0e95edf95b51ca85ca18af28854c78acc05a1ec2" } dependencies = [ { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] @@ -4044,7 +4044,7 @@ wheels = [ [[package]] name = "verifiers" version = "0.1.12.dev5" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=1d308d2#1d308d28da72ee170119c14c155c33f24a25ec56" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=0e95edf#0e95edf95b51ca85ca18af28854c78acc05a1ec2" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From fcaa83409eedee3f7ecf649cfe3d5d3615f01acc Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 23 Apr 2026 00:28:32 +0530 Subject: [PATCH 30/54] chore: bump verifiers to f47ba32 (gated synthesize-close bridge) Picks up the two-part bridge truncation fix: - 9c5ad2bf synthesize the renderer's preferred close when vLLM truncated the prior completion - f47ba321 gate the synthesize on a per-renderer opt-in classvar; hand- coded renderers (Qwen3, GLM, DeepSeek V3, Kimi, MiniMax-M2, Nemotron3, GPT-OSS) opt in; DefaultRenderer stays off and falls back to a full re-render to mirror main's TITO-on-truncation behavior Expected effect on wordle (DefaultRenderer, PrimeIntellect/Qwen3-1.7B-Wordle-SFT): samples_per_rollout drops from 1.34 to ~1.22 (parity with main), trainer forward_backward drops ~10s/step. On models with a hand-coded renderer, samples_per_rollout should approach 1.0 (token-exact extension). Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e7f5da6d238..ceaf52021e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "0e95edf" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "f47ba32" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] diff --git a/uv.lock b/uv.lock index df56997336b..baf5107d2c4 100644 --- a/uv.lock +++ b/uv.lock @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=0e95edf" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=f47ba32" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3249,7 +3249,7 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=0e95edf#0e95edf95b51ca85ca18af28854c78acc05a1ec2" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=f47ba32#f47ba3219003e68fbdf966738b4a61863d456752" } dependencies = [ { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] @@ -4044,7 +4044,7 @@ wheels = [ [[package]] name = "verifiers" version = "0.1.12.dev5" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=0e95edf#0e95edf95b51ca85ca18af28854c78acc05a1ec2" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=f47ba32#f47ba3219003e68fbdf966738b4a61863d456752" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From 858aa45ab123ebc5451d424f6183c20e991d2290 Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 23 Apr 2026 01:45:22 +0530 Subject: [PATCH 31/54] chore: bump verifiers to fb6ecb9 (bridge anchor fix + synth-close knob) Includes the fix where _get_incremental_prompt_ids now lets truncated steps anchor the bridge when synth_ok is true, plus a configurable synthesize_close_on_truncation kwarg on DefaultRenderer. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ceaf52021e0..ecb8d19ac3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "f47ba32" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "fb6ecb9" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] From cd56374bb635507c07c1b1163c5704afd62677ce Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 23 Apr 2026 03:04:19 +0530 Subject: [PATCH 32/54] chore: bump verifiers to 891da50 (per-renderer bridge_to_next_turn) Picks up the bridge refactor: each hand-coded renderer owns its own truncation / next-turn-opener logic via bridge_to_next_turn, driven by shared chatml_bridge and glm_bridge helpers. DefaultRenderer continues to fall back to the generic algorithm gated on synthesize_close_on_truncation. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ecb8d19ac3f..1fb8f9df51b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "fb6ecb9" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "891da50" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] From c06a0afe764701748412d13e0eee839a88619c33 Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 23 Apr 2026 04:20:58 +0530 Subject: [PATCH 33/54] chore: bump verifiers to 9020f70 (kimi/gpt-oss parse fixes + wider round-trip) Picks up: - parse_gpt_oss now handles multi-tool-call assistants (stops truncating at the first <|call|>) - Kimi K2/K2.5/K2.6 renderers emit tool_call ids in the template's functions.{name}:{idx} shape so round-trip recovers the function name - Kimi K2 no longer strips reasoning from the most-recent assistant turn - Round-trip test matrix expanded to 15 renderers (adds Qwen3-VL, GLM-5.1, Kimi K2.x, Nemotron-3 Nano+Super, GPT-OSS) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1fb8f9df51b..7a4d0113287 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "891da50" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "9020f70" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] From 6e99c2e8fbf5717d60c740e289f11c0c49e658dc Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 23 Apr 2026 06:17:47 +0530 Subject: [PATCH 34/54] chore: bump verifiers to 7d2f7f7 (full render parity across the matrix) Picks up both parity waves for the expanded renderer matrix: - GLM-5.1 (new GLM51Renderer), Kimi K2, Nemotron-3 Nano+Super are now byte-identical to apply_chat_template for the baseline + tool cases - Kimi K2 and Nemotron-3 correctly translate auto-injected message indices back to the caller's input list so build_supervised_sample doesn't dereference past the end - MiniMax consecutive tool_response BPE boundary fixed Remaining gap: one xfail for Qwen3-VL's content=None upstream Jinja crash (HF template bug, not ours). --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7a4d0113287..f031a1d9fe5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "9020f70" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "7d2f7f7" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] From 4aee0c6441eef00aa062b420e1b7efb52c21cf52 Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 23 Apr 2026 07:12:20 +0530 Subject: [PATCH 35/54] chore: bump verifiers to 6f89c38 (hand-coded per-renderer bridges) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the shared chatml_bridge/glm_bridge dummy-assistant tricks with hand-written bridge_to_next_turn on every renderer, eliminating silent divergence between render and bridge paths. No prime-rl behaviour change — the client already called bridge_to_next_turn through the renderer interface. Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f031a1d9fe5..893581cd2a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "7d2f7f7" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "6f89c38" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] diff --git a/uv.lock b/uv.lock index baf5107d2c4..9333c475102 100644 --- a/uv.lock +++ b/uv.lock @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=f47ba32" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=6f89c38" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3249,7 +3249,7 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=f47ba32#f47ba3219003e68fbdf966738b4a61863d456752" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=6f89c38#6f89c3846ed1156aa2390c36d9e7fcbd2d5cc2f6" } dependencies = [ { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] @@ -4044,7 +4044,7 @@ wheels = [ [[package]] name = "verifiers" version = "0.1.12.dev5" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=f47ba32#f47ba3219003e68fbdf966738b4a61863d456752" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=6f89c38#6f89c3846ed1156aa2390c36d9e7fcbd2d5cc2f6" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From 62946a79fbec3ef415542e63b00d03bc300c1976 Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 23 Apr 2026 15:23:24 +0530 Subject: [PATCH 36/54] fix(serving_generate): plumb cache_salt through /v1/generate GenerateRequest now accepts cache_salt (optional str) and passes it into vLLM's tokens_input(...). Matches the contract of main's /v1/chat/completions/tokens: vLLM's prefix cache includes cache_salt in its block hash, so when the orchestrator bumps the salt per ckpt_step the stale KV is properly invalidated. Root cause of the 3x mismatch_kl drift renderers path exhibited vs main; salt fix collapses wordle KL from drifting-to-0.002 to flat at main's 0.0006 baseline, and reverse-text from 0.019 to 0.004. Paired with the companion fix in verifiers' renderer client (which was dropping cache_salt from extra_body before sending to /generate). Local only, unsigned. --- src/prime_rl/inference/vllm/serving_generate.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/prime_rl/inference/vllm/serving_generate.py b/src/prime_rl/inference/vllm/serving_generate.py index 91f20a28568..594f603746d 100644 --- a/src/prime_rl/inference/vllm/serving_generate.py +++ b/src/prime_rl/inference/vllm/serving_generate.py @@ -52,6 +52,15 @@ class GenerateRequest(BaseModel): min_tokens: int = 0 prompt_logprobs: bool = False priority: int = 0 + # Prefix-cache invalidation salt. Must match main's + # /v1/chat/completions/tokens path: the orchestrator sets + # `extra_body["cache_salt"] = str(ckpt_step)` on every rollout + # request. vLLM's KV cache hashes include this salt, so when the + # step changes the cache misses and KV is recomputed with fresh + # weights. Without this, renderers path silently reuses stale KV + # from before the latest weight update and its logprobs drift from + # the trainer's forward pass (mismatch_kl grows 3x over training). + cache_salt: str | None = None class GenerateChoiceResponse(BaseModel): @@ -84,7 +93,7 @@ def __init__(self, engine_client: EngineClient, chat_handler: Any | None = None) async def generate(self, request: GenerateRequest, raw_request: Request) -> GenerateResponse | dict: # Pre-rendered TokensInput shape (type="token") — avoids vLLM's # "raw prompt" deprecation that targets plain lists/strings. - engine_prompt = tokens_input(request.prompt_token_ids) + engine_prompt = tokens_input(request.prompt_token_ids, cache_salt=request.cache_salt) # Match /v1/chat/completions: if the client didn't ask for a specific # cap, let the model generate up to whatever room is left in context. From 67f9e54e5a38ccebc938ef77ad6da36ff62aafe7 Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 23 Apr 2026 15:48:08 +0530 Subject: [PATCH 37/54] chore: bump verifiers to c499099 (cache_salt forwarding on /v1/generate) Pairs with the prior prime-rl change that plumbs cache_salt through GenerateRequest and into vLLM's tokens_input. Together they close the renderers-path KL-drift bug: wordle mismatch_kl was drifting from 0.0005 to 0.002 over 200 steps vs main's flat 0.0006; with the salt reaching vLLM, renderers is now flat at 0.0006 matching main exactly (verified through step 40 of 100-step test). Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 893581cd2a2..c7d729dbaaa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "6f89c38" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "c499099" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] diff --git a/uv.lock b/uv.lock index 9333c475102..297e0a9b8d5 100644 --- a/uv.lock +++ b/uv.lock @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=6f89c38" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=c499099" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3249,7 +3249,7 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=6f89c38#6f89c3846ed1156aa2390c36d9e7fcbd2d5cc2f6" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=c499099#c4990998cb8fbda21e30d7008e4b0c4bbfbacde7" } dependencies = [ { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] @@ -4044,7 +4044,7 @@ wheels = [ [[package]] name = "verifiers" version = "0.1.12.dev5" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=6f89c38#6f89c3846ed1156aa2390c36d9e7fcbd2d5cc2f6" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=c499099#c4990998cb8fbda21e30d7008e4b0c4bbfbacde7" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From 889d15ec57e80bb88d07d4eb802588124a3ed1cd Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 23 Apr 2026 16:42:22 +0530 Subject: [PATCH 38/54] =?UTF-8?q?chore:=20bump=20verifiers=20to=20fafea87?= =?UTF-8?q?=20(exact-name=20model=E2=86=92renderer=20map)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pairs with the upstream change that flips MODEL_RENDERER_MAP from longest-prefix to exact-match. Prefix matching silently mapped fine-tunes to same-arch renderers even when their chat template had diverged — concrete miss during this comparison was PrimeIntellect/Qwen3-1.7B-Wordle-SFT not matching Qwen/Qwen3*, so it fell to DefaultRenderer at half the speed of the hand-coded Qwen3Renderer. Exact-match surfaces the mismatch as a loud INFO log and requires renderer= explicitly for fine-tunes. Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c7d729dbaaa..8eab8a32c6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "c499099" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "fafea87" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] diff --git a/uv.lock b/uv.lock index 297e0a9b8d5..2245367ae8d 100644 --- a/uv.lock +++ b/uv.lock @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=c499099" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=fafea87" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3249,7 +3249,7 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=c499099#c4990998cb8fbda21e30d7008e4b0c4bbfbacde7" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=fafea87#fafea87b5a9bb0eef2547841bfb6fd4958b96907" } dependencies = [ { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] @@ -4044,7 +4044,7 @@ wheels = [ [[package]] name = "verifiers" version = "0.1.12.dev5" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=c499099#c4990998cb8fbda21e30d7008e4b0c4bbfbacde7" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=fafea87#fafea87b5a9bb0eef2547841bfb6fd4958b96907" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From 7bbf985c893a7d263bc4a0023f00e47180a4f3cf Mon Sep 17 00:00:00 2001 From: hallerite Date: Fri, 24 Apr 2026 21:15:48 +0530 Subject: [PATCH 39/54] fix(ring_attn): adapt FA3 varlen wrappers to flash_attn_3 3.0.0 kwargs Cherry-picked from PR #2352 (main branch). flash_attn_3 3.0.0 renamed `causal` -> `is_causal` and split `window_size` into `window_size_left` / `window_size_right`. Required for Qwen3.5 MoE or any cp>1 + FA3 + custom-impl config to train without crashing the first backward pass. --- src/prime_rl/trainer/models/layers/ring_attn.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/prime_rl/trainer/models/layers/ring_attn.py b/src/prime_rl/trainer/models/layers/ring_attn.py index 0db4b53c873..34e30e41c6a 100644 --- a/src/prime_rl/trainer/models/layers/ring_attn.py +++ b/src/prime_rl/trainer/models/layers/ring_attn.py @@ -30,11 +30,11 @@ def _fa3_varlen_forward( "max_seqlen_q": max_seqlen_q, "max_seqlen_k": max_seqlen_k, "softmax_scale": softmax_scale, - "causal": causal, + "is_causal": causal, + "window_size_left": window_size[0], + "window_size_right": window_size[1], } ) - if "window_size" in params: - params["window_size"] = window_size out, lse, _, _ = _flash_attn_forward(**params) return out, lse @@ -76,11 +76,11 @@ def _fa3_varlen_backward( "dk": dk, "dv": dv, "softmax_scale": softmax_scale, - "causal": causal, + "is_causal": causal, + "window_size_left": window_size[0], + "window_size_right": window_size[1], } ) - if "window_size" in params: - params["window_size"] = window_size _flash_attn_backward(**params) From e099a9fb07c8634b2aacc1ac8aee647dfdc42c14 Mon Sep 17 00:00:00 2001 From: hallerite Date: Fri, 24 Apr 2026 22:13:09 +0530 Subject: [PATCH 40/54] chore: bump verifiers to c58da4a Brings in: dba2e97 fix(parse_qwen3): fall back to raw content when tool_call JSON malformed 5d65e3f feat(client): categorize extension breaks in _get_incremental_prompt_ids c58da4a docs(renderers): expand README for external audience Production-relevant substance is the parse_qwen3 hermes-style fallback on malformed tool-call JSON (prevents EmptyModelResponseError cascades on hermes-tool envs with unfinetuned models) and the DEBUG break-category telemetry in the incremental prompt path. README rewrite is docs-only. --- pyproject.toml | 2 +- uv.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8eab8a32c6f..78c60499c92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "fafea87" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "c58da4a" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] diff --git a/uv.lock b/uv.lock index 2245367ae8d..5a76146cc9f 100644 --- a/uv.lock +++ b/uv.lock @@ -873,8 +873,8 @@ dependencies = [ { name = "torch", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/test/cu128/flash_attn_3-3.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:10a730d5e1a1c23afd930ce419ee425cd1925a16b7eac789dfe98c3ca39bc009" }, - { url = "https://download.pytorch.org/whl/test/cu128/flash_attn_3-3.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:bbb51d13d7aa1bfe04ef8875ed1bf630ebb116775084a5df612495e10609cd76" }, + { url = "https://download.pytorch.org/whl/test/cu128/flash_attn_3-3.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:10a730d5e1a1c23afd930ce419ee425cd1925a16b7eac789dfe98c3ca39bc009", upload-time = "2026-02-14T04:28:12Z" }, + { url = "https://download.pytorch.org/whl/test/cu128/flash_attn_3-3.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:bbb51d13d7aa1bfe04ef8875ed1bf630ebb116775084a5df612495e10609cd76", upload-time = "2026-02-14T04:28:05Z" }, ] [[package]] @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=fafea87" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=c58da4a" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3249,7 +3249,7 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=fafea87#fafea87b5a9bb0eef2547841bfb6fd4958b96907" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=c58da4a#c58da4ab491f090f3369c200ad52b7a9215c2817" } dependencies = [ { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] @@ -3753,8 +3753,8 @@ dependencies = [ { name = "typing-extensions", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6f09cdf2415516be028ae82e6b985bcfc3eac37bc52ab401142689f6224516ca" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:628e89bd5110ced7debee2a57c69959725b7fbc64eab81a39dd70e46c7e28ba5" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6f09cdf2415516be028ae82e6b985bcfc3eac37bc52ab401142689f6224516ca", upload-time = "2026-01-21T15:22:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:628e89bd5110ced7debee2a57c69959725b7fbc64eab81a39dd70e46c7e28ba5", upload-time = "2026-01-21T15:22:11Z" }, ] [[package]] @@ -4044,7 +4044,7 @@ wheels = [ [[package]] name = "verifiers" version = "0.1.12.dev5" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=fafea87#fafea87b5a9bb0eef2547841bfb6fd4958b96907" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=c58da4a#c58da4ab491f090f3369c200ad52b7a9215c2817" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From 96418714e6990fb2ccf02677d9b901f92588b502 Mon Sep 17 00:00:00 2001 From: hallerite Date: Sat, 25 Apr 2026 01:40:02 +0530 Subject: [PATCH 41/54] chore: bump verifiers to 7136de5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picks up the new Qwen3.6 renderer — wires renderer="qwen3.6" in the new cmp_rlm_swe_renderers.toml (Qwen3.6-35B-A3B + RLM + R2E-Gym). Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 78c60499c92..af343e8a10c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "c58da4a" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "7136de5" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] diff --git a/uv.lock b/uv.lock index 5a76146cc9f..d3a128b981d 100644 --- a/uv.lock +++ b/uv.lock @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=c58da4a" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=7136de5" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3249,7 +3249,7 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=c58da4a#c58da4ab491f090f3369c200ad52b7a9215c2817" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=7136de5#7136de5f2468c32e173195547ea225837b6958c2" } dependencies = [ { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] @@ -4044,7 +4044,7 @@ wheels = [ [[package]] name = "verifiers" version = "0.1.12.dev5" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=c58da4a#c58da4ab491f090f3369c200ad52b7a9215c2817" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=7136de5#7136de5f2468c32e173195547ea225837b6958c2" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From 8e7b68f4461baa54df7a02e14df30bf5dc5d66cb Mon Sep 17 00:00:00 2001 From: hallerite Date: Sat, 25 Apr 2026 01:50:14 +0530 Subject: [PATCH 42/54] chore: bump verifiers to 7b86d14 (merge of main into renderers) Brings in the updated rlm_harness surface (rlm_tools, summarize_at_tokens, allow_git, local_checkout, rlm_ref) needed by rlm_swe>=0.3.0, plus all other main-branch work through v0.1.13.dev7. Verifiers now exposes our Qwen3.6 renderer from the same tree. Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- uv.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index af343e8a10c..0814fbf7561 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "7136de5" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "7b86d14" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] diff --git a/uv.lock b/uv.lock index d3a128b981d..0bafcc62f4b 100644 --- a/uv.lock +++ b/uv.lock @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=7136de5" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=7b86d14" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -2818,7 +2818,7 @@ mamba-ssm = [{ name = "mamba-ssm", specifier = ">=2.3.0" }] [[package]] name = "prime-sandboxes" -version = "0.2.19" +version = "0.2.22" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -2828,9 +2828,9 @@ dependencies = [ { name = "pydantic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "tenacity", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/8c/7d8b8013f65d8394094a922bc493eaae09e5e88058f80bd009637ffeff63/prime_sandboxes-0.2.19.tar.gz", hash = "sha256:081cd9ef246d397b4a2fdd77a94679a5f08115ecbbe3f90f320e5f4a7988820e", size = 60558, upload-time = "2026-04-01T00:46:28.212Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/a7/eb9cb01ce7fa812022ec7c2ffaeb379e1103e07f093a4b85e879b2d0143d/prime_sandboxes-0.2.22.tar.gz", hash = "sha256:7901adca4ff7fff1e7f08c06c4b76765cd90791965216655eb3a87d459588f5e", size = 64970, upload-time = "2026-04-22T18:32:12.973Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/56/9bd4b3733773e6c122ebfe92144476c143666d09d9e8cccb9d1349d4d43c/prime_sandboxes-0.2.19-py3-none-any.whl", hash = "sha256:ce5db7314c007aae796e1ce274181eaa1370000a478bcd55c26149edfee580c1", size = 31617, upload-time = "2026-04-01T00:46:27.216Z" }, + { url = "https://files.pythonhosted.org/packages/90/b8/46bf98c06826fc26d982c53e10d9d4f086a4c322993a6c51188e5bd6fd30/prime_sandboxes-0.2.22-py3-none-any.whl", hash = "sha256:0a75bb02049c16b55aae7475cc4082e8559ffa6d65181b238077771a95b57855", size = 33182, upload-time = "2026-04-22T18:32:11.739Z" }, ] [[package]] @@ -3249,7 +3249,7 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=7136de5#7136de5f2468c32e173195547ea225837b6958c2" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=7b86d14#7b86d14e73955caecd897af965f9f024ee8b235b" } dependencies = [ { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] @@ -4043,8 +4043,8 @@ wheels = [ [[package]] name = "verifiers" -version = "0.1.12.dev5" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=7136de5#7136de5f2468c32e173195547ea225837b6958c2" } +version = "0.1.13.dev7" +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=7b86d14#7b86d14e73955caecd897af965f9f024ee8b235b" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From 3ba3dcd199536ec918761457886178a9db4ad11b Mon Sep 17 00:00:00 2001 From: Jackmin801 <56836461+Jackmin801@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:38:47 -0700 Subject: [PATCH 43/54] [fix] patch gdn with varlen for qwen (#2346) * patch gdn with varlen * claude just casually reverted this what? --- src/prime_rl/trainer/model.py | 169 ++++++++++++++++++ .../qwen3_5_moe/modeling_qwen3_5_moe.py | 30 +++- 2 files changed, 195 insertions(+), 4 deletions(-) diff --git a/src/prime_rl/trainer/model.py b/src/prime_rl/trainer/model.py index 3b3020b8003..b86e008e601 100644 --- a/src/prime_rl/trainer/model.py +++ b/src/prime_rl/trainer/model.py @@ -126,6 +126,173 @@ def _patched_forward(self, hidden_states, position_ids=None, **kwargs): decoder_layer_cls.forward = _make_patched_forward(_original_forward) +def _patch_qwen3_5_linear_attn_varlen(): + """Thread cu_seqlens through Qwen3.5 GatedDeltaNet so packed batches don't + leak conv/SSM state across sequences. + + HF's forward hardcodes seq_idx=None for causal_conv1d and omits cu_seqlens + for chunk_gated_delta_rule, so packed RL training sees ~0.23 Mismatch KL vs + vLLM (target <0.01). Mirrors the NemotronH mamba fix. + """ + import torch.nn.functional as F + from transformers.models.qwen3_5.modeling_qwen3_5 import ( + Qwen3_5DecoderLayer, + Qwen3_5GatedDeltaNet, + Qwen3_5TextModel, + apply_mask_to_padding_states, + ) + + if getattr(Qwen3_5GatedDeltaNet.forward, "_prl_varlen_patched", False): + return + + _gdn_orig = Qwen3_5GatedDeltaNet.forward + + def _gdn_forward(self, hidden_states, cache_params=None, attention_mask=None, cu_seqlens=None): + if cu_seqlens is None or cache_params is not None: + return _gdn_orig(self, hidden_states, cache_params=cache_params, attention_mask=attention_mask) + + hidden_states = apply_mask_to_padding_states(hidden_states, attention_mask) + batch_size, seq_len, _ = hidden_states.shape + + mixed_qkv = self.in_proj_qkv(hidden_states).transpose(1, 2) + z = self.in_proj_z(hidden_states).reshape(batch_size, seq_len, -1, self.head_v_dim) + b = self.in_proj_b(hidden_states) + a = self.in_proj_a(hidden_states) + + if self.causal_conv1d_fn is not None: + seg_lens = cu_seqlens[1:] - cu_seqlens[:-1] + seq_idx = torch.repeat_interleave( + torch.arange(seg_lens.numel(), dtype=torch.int32, device=hidden_states.device), + seg_lens, + ).unsqueeze(0) + mixed_qkv = self.causal_conv1d_fn( + x=mixed_qkv, + weight=self.conv1d.weight.squeeze(1), + bias=self.conv1d.bias, + activation=self.activation, + seq_idx=seq_idx, + ) + else: + # Per-segment conv1d so the kernel-1 left pad only draws from within each sequence. + cu = cu_seqlens.tolist() + conv_outs = [] + for i in range(len(cu) - 1): + s, e = cu[i], cu[i + 1] + if s == e: + continue + conv_outs.append(self.conv1d(mixed_qkv[:, :, s:e])[:, :, : e - s]) + mixed_qkv = F.silu(torch.cat(conv_outs, dim=-1)) + + mixed_qkv = mixed_qkv.transpose(1, 2) + query, key, value = torch.split(mixed_qkv, [self.key_dim, self.key_dim, self.value_dim], dim=-1) + query = query.reshape(batch_size, seq_len, -1, self.head_k_dim) + key = key.reshape(batch_size, seq_len, -1, self.head_k_dim) + value = value.reshape(batch_size, seq_len, -1, self.head_v_dim) + + beta = b.sigmoid() + g = -self.A_log.float().exp() * F.softplus(a.float() + self.dt_bias) + if self.num_v_heads // self.num_k_heads > 1: + query = query.repeat_interleave(self.num_v_heads // self.num_k_heads, dim=2) + key = key.repeat_interleave(self.num_v_heads // self.num_k_heads, dim=2) + + core_attn_out, _ = self.chunk_gated_delta_rule( + query, + key, + value, + g=g, + beta=beta, + initial_state=None, + output_final_state=False, + use_qk_l2norm_in_kernel=True, + cu_seqlens=cu_seqlens, + ) + + core_attn_out = core_attn_out.reshape(-1, self.head_v_dim) + z = z.reshape(-1, self.head_v_dim) + core_attn_out = self.norm(core_attn_out, z) + core_attn_out = core_attn_out.reshape(batch_size, seq_len, -1) + return self.out_proj(core_attn_out) + + _gdn_forward._prl_varlen_patched = True + Qwen3_5GatedDeltaNet.forward = _gdn_forward + + _dec_orig = Qwen3_5DecoderLayer.forward + + def _dec_forward( + self, + hidden_states, + position_embeddings, + attention_mask=None, + position_ids=None, + past_key_values=None, + cu_seqlens=None, + **kwargs, + ): + if position_ids is not None and position_ids.ndim == 3: + position_ids = position_ids[0] + if self.layer_type != "linear_attention": + return _dec_orig( + self, + hidden_states, + position_embeddings=position_embeddings, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + **kwargs, + ) + + residual = hidden_states + hidden_states = self.input_layernorm(hidden_states) + hidden_states = self.linear_attn( + hidden_states=hidden_states, + cache_params=past_key_values, + attention_mask=attention_mask, + cu_seqlens=cu_seqlens, + ) + hidden_states = residual + hidden_states + residual = hidden_states + hidden_states = self.post_attention_layernorm(hidden_states) + hidden_states = self.mlp(hidden_states) + return residual + hidden_states + + Qwen3_5DecoderLayer.forward = _dec_forward + + _text_orig = Qwen3_5TextModel.forward + + def _text_forward( + self, + input_ids=None, + attention_mask=None, + position_ids=None, + past_key_values=None, + inputs_embeds=None, + use_cache=None, + **kwargs, + ): + attn_impl = getattr(self.config, "_attn_implementation", None) + cu_seqlens = None + if attn_impl in ("flash_attention_2", "flash_attention_3", "fa4") and position_ids is not None: + pids = position_ids + if pids.ndim == 3: + pids = pids[0] + flat = pids.view(-1) + seqlens = torch.cat([flat[0:1], flat[:-1][(flat == 0)[1:]] + 1, flat[-1:] + 1]) + cu_seqlens = seqlens.cumsum(dim=0, dtype=torch.int32) + kwargs["cu_seqlens"] = cu_seqlens + return _text_orig( + self, + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + **kwargs, + ) + + Qwen3_5TextModel.forward = _text_forward + + # Add filter to the standard logging module for transformers.modeling_utils to supress the # flash attention dtype warnings since FSDP is used to handle mixed precision. transformers_modeling_utils_logger = logging.getLogger("transformers.modeling_utils") @@ -238,6 +405,7 @@ def get_model( if "Qwen3.5" in config.name or "qwen3_5" in config.name.lower(): _patch_qwen3_5_text_position_ids() _patch_qwen3_5_moe_conversion_mapping() + _patch_qwen3_5_linear_attn_varlen() model_config = cast( PretrainedConfig, @@ -276,6 +444,7 @@ def get_model( if getattr(model_config, "model_type", "").startswith("qwen3_5_moe"): _patch_qwen3_5_text_position_ids() _patch_qwen3_5_moe_conversion_mapping() + _patch_qwen3_5_linear_attn_varlen() for subconfig_key in getattr(model_config, "sub_configs", {}): subconfig = getattr(model_config, subconfig_key, None) if subconfig is not None and hasattr(subconfig, "use_cache"): diff --git a/src/prime_rl/trainer/models/qwen3_5_moe/modeling_qwen3_5_moe.py b/src/prime_rl/trainer/models/qwen3_5_moe/modeling_qwen3_5_moe.py index d26ebc627d0..932cecb5736 100644 --- a/src/prime_rl/trainer/models/qwen3_5_moe/modeling_qwen3_5_moe.py +++ b/src/prime_rl/trainer/models/qwen3_5_moe/modeling_qwen3_5_moe.py @@ -255,7 +255,11 @@ def _build_cp_context(self, local_seq_len: int, device: torch.device) -> "FLACPC conv1d_kernel_size=self.conv_kernel_size, ) - def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + def forward( + self, + hidden_states: torch.Tensor, + cu_seqlens: torch.LongTensor | None = None, + ) -> torch.Tensor: batch_size, seq_len, _ = hidden_states.shape mixed_qkv = self.in_proj_qkv(hidden_states).transpose(1, 2) @@ -263,15 +267,32 @@ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: b = self.in_proj_b(hidden_states) a = self.in_proj_a(hidden_states) - # Causal conv1d + # Causal conv1d — must reset at sequence boundaries for packed batches, + # otherwise the kernel-1 left pad leaks state across sequences. if self._causal_conv1d_fn is not None: + seq_idx = None + if cu_seqlens is not None: + seg_lens = cu_seqlens[1:] - cu_seqlens[:-1] + seq_idx = torch.repeat_interleave( + torch.arange(seg_lens.numel(), dtype=torch.int32, device=hidden_states.device), + seg_lens, + ).unsqueeze(0) mixed_qkv = self._causal_conv1d_fn( x=mixed_qkv, weight=self.conv1d.weight.squeeze(1), bias=self.conv1d.bias, activation=self.activation, - seq_idx=None, + seq_idx=seq_idx, ) + elif cu_seqlens is not None: + cu = cu_seqlens.tolist() + conv_outs = [] + for i in range(len(cu) - 1): + s, e = cu[i], cu[i + 1] + if s == e: + continue + conv_outs.append(self.conv1d(mixed_qkv[:, :, s:e])[:, :, : e - s]) + mixed_qkv = F.silu(torch.cat(conv_outs, dim=-1)) else: mixed_qkv = F.silu(self.conv1d(mixed_qkv)[:, :, :seq_len]) @@ -313,6 +334,7 @@ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: initial_state=None, output_final_state=False, use_qk_l2norm_in_kernel=True, + cu_seqlens=cu_seqlens, ) # Gated RMSNorm @@ -606,7 +628,7 @@ def forward( # Token mixer if self.layer_type == "linear_attention": - hidden_states = self.linear_attn(hidden_states) + hidden_states = self.linear_attn(hidden_states, cu_seqlens=cu_seqlens) elif self.layer_type == "full_attention": hidden_states, _ = self.self_attn( hidden_states=hidden_states, From 8089985bdacd4cd7a1fa42401627c1276a61c89c Mon Sep 17 00:00:00 2001 From: hallerite Date: Wed, 29 Apr 2026 00:35:57 +0530 Subject: [PATCH 44/54] chore(deps): bump verifiers to 17358ee MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picks up the renderers branch state from 7b86d14 → 17358ee. Highlights in the bump: - ``feat(renderers/gpt_oss)``: rewrite as openai-harmony adapter (matches vLLM's wire format byte-for-byte; superset of features over the previous hand-rolled implementation). - ``fix(renderers)``: K2.5 / K2.6 token-for-token parity with ``apply_chat_template`` (graduates them into the parity matrix). - ``fix(renderers/kimi_k25)``: tools encoder accepts both flat ``ToolSpec`` and OpenAI envelope shapes. - ``fix(client)``: ``_attach_tool_call_names`` recovers the function name on tool-result messages so GPT-OSS no longer renders ``functions.unknown``. - ``feat(client)``: full vLLM ``/generate`` kwargs allowlist + ``extra_headers`` passthrough. - ``chore(clients)``: restore the ``OpenAIChatCompletionsTokenClient`` (TITO) alongside ``RendererClient`` so both paths coexist. - ``refactor(renderers)``: rename ``build_supervised_sample`` → ``build_training_sample``. - Style: ruff format clean across renderers package + tests. Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- uv.lock | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0814fbf7561..08595b50a29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ deep-gemm = { url = "https://github.com/samsja/research-pre-built-wheels/release nixl-cu12 = { url = "https://github.com/samsja/research-pre-built-wheels/releases/download/v0.2/nixl_cu12-0.10.1-cp312-cp312-linux_x86_64.whl" } flash-linear-attention = { git = "https://github.com/fla-org/flash-linear-attention" } flash_attn_3 = { index = "pytorch-cu128-test" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "7b86d14" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "17358ee" } [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] diff --git a/uv.lock b/uv.lock index 0bafcc62f4b..61f421fc818 100644 --- a/uv.lock +++ b/uv.lock @@ -2306,17 +2306,17 @@ wheels = [ [[package]] name = "openai-harmony" -version = "0.0.4" +version = "0.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/94/01509d510bebf6606614e51113e5a415ced15b8f34aa98a8bf2539314650/openai_harmony-0.0.4.tar.gz", hash = "sha256:5c67ac6df349236fb7b64f57c3dbb0273efcdca24314daa108f2a482c427106c", size = 279848, upload-time = "2025-08-09T01:43:24.974Z" } +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/05/88/ade63bd8f36603610040e7cc086bc134d57a99a742e05f7fcddfdf822ee1/openai_harmony-0.0.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf2344366f10981bbc0f6d9949a0b2bb87151d209ed295943ed6ad8eda37932", size = 2963206, upload-time = "2025-08-09T01:43:02.433Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ec/dcdcace0ffcf3a532cca910e0c351b62d3a7decf0b091ea8cf856d2a67a6/openai_harmony-0.0.4-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31e9bcac0902a309e2fc688e52f247eec7fffcd00d17e958b9a83a8fea6519c2", size = 3049306, upload-time = "2025-08-09T01:43:11.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/39/172f1048d935db1523a82b45fee5231ad6c622645e566706e6bcf3731da8/openai_harmony-0.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:96a63199c0d81095b5d5d1ae8ca82b64c1c13d18d4e30323ae9e8ab31bc80a3d", size = 3121347, upload-time = "2025-08-09T01:43:16.705Z" }, - { url = "https://files.pythonhosted.org/packages/a8/bd/aa9e6e5cf140716dbcae17402fac2a81a9ebb3f934059ac0eec61cb447fc/openai_harmony-0.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:15e6d53a66502491a3675a536df30e271f976e6c5efe68250a65191efcb85c4f", size = 3221129, upload-time = "2025-08-09T01:43:23.146Z" }, + { 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/25/3f/1a192b93bb47c6b44cd98ba8cc1d3d2a9308f1bb700c3017e6352da11bda/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c007d277218a50db8839e599ed78e0fffe5130f614c3f6d93ae257f282071a29", size = 2953260, upload-time = "2025-11-05T19:06:55.406Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/93b582cad3531797c3db7c2db5400fd841538ccddfd9f5e3df61be99a630/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8565d4f5a0638da1bffde29832ed63c9e695c558611053add3b2dc0b56c92dbc", size = 3127044, upload-time = "2025-11-05T19:06:59.553Z" }, + { url = "https://files.pythonhosted.org/packages/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" }, ] [[package]] @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=7b86d14" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=17358ee" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3249,8 +3249,9 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=7b86d14#7b86d14e73955caecd897af965f9f024ee8b235b" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=17358ee#17358eef8f158696804c4ddde129963b360b71aa" } dependencies = [ + { name = "openai-harmony", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] @@ -4044,7 +4045,7 @@ wheels = [ [[package]] name = "verifiers" version = "0.1.13.dev7" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=7b86d14#7b86d14e73955caecd897af965f9f024ee8b235b" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=17358ee#17358eef8f158696804c4ddde129963b360b71aa" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From 01f945300837e792cd96dfb5ac2db0ef85f441d1 Mon Sep 17 00:00:00 2001 From: hallerite Date: Wed, 29 Apr 2026 00:36:14 +0530 Subject: [PATCH 45/54] chore(inference): restore /v1/chat/completions/tokens (TITO) alongside /v1/generate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both inference endpoints now coexist on this branch — clients can opt into either via ``ClientConfig.client_type``: - ``"openai_chat_completions_token"`` → server-side templating, token IDs returned alongside the chat completion (TITO, ``/v1/chat/completions/tokens``). - ``"renderer"`` → client-side tokenization through the ``renderers`` package, served via ``/v1/generate``. Files restored verbatim from origin/main: - ``src/prime_rl/inference/vllm/serving_chat_with_tokens.py``: the ``OpenAIServingChatWithTokens`` handler that augments vLLM's chat completion stream with token IDs. Server plumbing in ``src/prime_rl/inference/vllm/server.py``: - Re-add the ``ChatCompletionRequestWithTokens`` / ``OpenAIServingChatWithTokens`` imports and the supporting ``HTTPStatus``, ``Depends``, ``StreamingResponse``, ``ChatCompletionResponse``, ``validate_json_request`` imports. - Add a ``chat_with_tokens`` accessor and the ``/v1/chat/completions/tokens`` endpoint alongside the existing ``generate_handler`` and ``/v1/generate`` route. - ``custom_init_app_state`` now wraps ``state.openai_serving_chat`` in ``OpenAIServingChatWithTokens`` (when ``"generate"`` is in ``supported_tasks`` and chat is available) and stores the wrapped handler under both ``state.openai_serving_chat`` and ``state.openai_serving_chat_with_tokens``, mirroring main's wiring. The ``/v1/generate`` endpoint and the renderer-pool initialisation are unchanged — both paths share the same engine state and only differ in how the request body is interpreted. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/prime_rl/inference/vllm/server.py | 56 +++- .../vllm/serving_chat_with_tokens.py | 256 ++++++++++++++++++ 2 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 src/prime_rl/inference/vllm/serving_chat_with_tokens.py diff --git a/src/prime_rl/inference/vllm/server.py b/src/prime_rl/inference/vllm/server.py index ddc48117700..6ea7ab848d4 100644 --- a/src/prime_rl/inference/vllm/server.py +++ b/src/prime_rl/inference/vllm/server.py @@ -1,16 +1,19 @@ from argparse import Namespace +from http import HTTPStatus from typing import Any import uvloop -from fastapi import APIRouter, Request -from fastapi.responses import JSONResponse +from fastapi import APIRouter, Depends, Request +from fastapi.responses import JSONResponse, StreamingResponse from starlette.datastructures import State from vllm.engine.protocol import EngineClient from vllm.entrypoints.openai.api_server import init_app_state +from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionResponse from vllm.entrypoints.openai.cli_args import make_arg_parser, validate_parsed_serve_args from vllm.entrypoints.openai.engine.protocol import ErrorResponse from vllm.entrypoints.openai.engine.serving import OpenAIServing from vllm.entrypoints.openai.models.serving import OpenAIServingModels +from vllm.entrypoints.openai.utils import validate_json_request from vllm.entrypoints.serve.lora.protocol import LoadLoRAAdapterRequest from vllm.entrypoints.utils import load_aware_call, with_cancellation from vllm.logger import init_logger @@ -136,6 +139,10 @@ def resolve_tool_call_parser(model_name: str, tool_call_parser: str | None) -> s monkey_patch_load_lora_adapter, monkey_patch_tokenize_params_validation, ) +from prime_rl.inference.vllm.serving_chat_with_tokens import ( + ChatCompletionRequestWithTokens, + OpenAIServingChatWithTokens, +) # NOTE: Fix harmony stop token propagation for GPT-OSS models # Upstream issue still open: https://github.com/vllm-project/vllm/issues/22519 @@ -175,6 +182,10 @@ def generate_handler(request: Request): return request.app.state.openai_serving_generate +def chat_with_tokens(request: Request) -> OpenAIServingChatWithTokens | None: + return request.app.state.openai_serving_chat_with_tokens + + @router.post("/v1/generate") @with_cancellation @load_aware_call @@ -192,6 +203,32 @@ async def _generate(raw_request: Request): return JSONResponse(content=result.model_dump()) +@router.post( + "/v1/chat/completions/tokens", + dependencies=[Depends(validate_json_request)], + responses={ + HTTPStatus.OK.value: {"content": {"text/event-stream": {}}}, + HTTPStatus.BAD_REQUEST.value: {"model": ErrorResponse}, + HTTPStatus.NOT_FOUND.value: {"model": ErrorResponse}, + HTTPStatus.INTERNAL_SERVER_ERROR.value: {"model": ErrorResponse}, + }, +) +@with_cancellation +@load_aware_call +async def _chat_with_tokens(request: ChatCompletionRequestWithTokens, raw_request: Request): + handler = chat_with_tokens(raw_request) + if handler is None: + return base(raw_request).create_error_response(message="The model does not support Chat Completions API") + generator = await handler.create_chat_completion_with_tokens(request, raw_request) + if isinstance(generator, ErrorResponse): + return JSONResponse(content=generator.model_dump(), status_code=generator.error.code) + + elif isinstance(generator, ChatCompletionResponse): + return JSONResponse(content=generator.model_dump()) + + return StreamingResponse(content=generator, media_type="text/event-stream") + + @router.post("/pause") async def pause(request: Request): await engine_client(request).pause_generation(mode="keep", clear_cache=False) @@ -246,12 +283,25 @@ async def custom_init_app_state( """ Modifies init_app_state: 1. Call the original init_app_state to set up standard state. - 2. Add /v1/generate endpoint for renderer-based token-level inference. + 2. Replace ``serving_chat`` with our ``OpenAIServingChatWithTokens`` wrapper + so the ``/v1/chat/completions/tokens`` (TITO) endpoint can stream + token IDs alongside the rendered chat completion. + 3. Add ``/v1/generate`` endpoint for renderer-based token-level inference. """ await init_app_state(engine_client, state, args, supported_tasks) state.reset_prefix_cache_after_update = getattr(args, "reset_prefix_cache_after_update", True) + # TITO: server-side chat templating + token IDs. + if "generate" in supported_tasks and state.openai_serving_chat is not None: + original_chat = state.openai_serving_chat + serving_chat = object.__new__(OpenAIServingChatWithTokens) + serving_chat.__dict__.update(original_chat.__dict__) + state.openai_serving_chat = serving_chat + state.openai_serving_chat_with_tokens = serving_chat + else: + state.openai_serving_chat_with_tokens = None + # /v1/generate endpoint — tokens + optional images, no chat template from prime_rl.inference.vllm.serving_generate import OpenAIServingGenerate diff --git a/src/prime_rl/inference/vllm/serving_chat_with_tokens.py b/src/prime_rl/inference/vllm/serving_chat_with_tokens.py new file mode 100644 index 00000000000..e7f4b911f64 --- /dev/null +++ b/src/prime_rl/inference/vllm/serving_chat_with_tokens.py @@ -0,0 +1,256 @@ +import base64 +from collections.abc import AsyncGenerator, AsyncIterator +from typing import ClassVar, Optional, Union + +import numpy as np +from fastapi import Request +from pydantic import Field +from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest, ChatCompletionResponse +from vllm.entrypoints.openai.chat_completion.serving import OpenAIServingChat +from vllm.entrypoints.openai.engine.protocol import ErrorResponse, RequestResponseMetadata +from vllm.entrypoints.openai.engine.serving import GenerationError +from vllm.entrypoints.utils import get_max_tokens +from vllm.exceptions import VLLMValidationError +from vllm.logger import init_logger +from vllm.outputs import RequestOutput +from vllm.reasoning import ReasoningParser +from vllm.sampling_params import BeamSearchParams, SamplingParams + +logger = init_logger(__name__) + + +class _RoutedExpertsCapture: + def __init__(self, generator: AsyncGenerator[RequestOutput, None]): + self._generator = generator + self.routed_experts: dict[int, dict] = {} + + def _encode_routed_experts(self, arr: np.ndarray) -> dict: + return { + "data": base64.b85encode(arr.tobytes()).decode("ascii"), + "shape": list(arr.shape), + } + + async def __aiter__(self): + async for request_output in self._generator: + for output in request_output.outputs: + if output.routed_experts is not None: + self.routed_experts[output.index] = self._encode_routed_experts(output.routed_experts) + yield request_output + + def post_process(self, response: ChatCompletionResponse): + for choice in response.choices: + if choice.index in self.routed_experts: + choice.routed_experts = self.routed_experts[choice.index] + + +class ChatCompletionRequestWithTokens(ChatCompletionRequest): + field_names: ClassVar[Optional[set[str]]] = None + tokens: list[int] = Field(description=("Prompt tokens to use for the request.")) + + +class OpenAIServingChatWithTokens(OpenAIServingChat): + """OpenAI-compatible generate API that allows token-in and routed experts capture.""" + + async def chat_completion_full_generator( + self, + request: ChatCompletionRequest, + result_generator: AsyncIterator[RequestOutput], + request_id: str, + model_name: str, + conversation, + tokenizer, + request_metadata: RequestResponseMetadata, + reasoning_parser: ReasoningParser | None = None, + ) -> ErrorResponse | ChatCompletionResponse: + # We need to override the full_generator to be able to capture the routed experts + # By default, VLLM does not save the routed experts into ChatCompletionResponse.choices, so we need to capture them manually + # How this works: + # 1. We create a custom generator that encapsulates the original result_generator in self._generator + # 2. We override it's __aiter__ method to also capture the routed experts as an extra field in ChatCompletionResponse.choices + # 3. We override the full_generator method to use the custom generator instead of the original one if expert routing is enabled + if self.model_config.enable_return_routed_experts: + capture = _RoutedExpertsCapture(result_generator) + result_generator = capture + else: + capture = None + + response = await super().chat_completion_full_generator( + request, + result_generator, + request_id, + model_name, + conversation, + tokenizer, + request_metadata, + reasoning_parser, + ) + + if capture and isinstance(response, ChatCompletionResponse): + capture.post_process(response) + + return response + + async def create_chat_completion_with_tokens( + self, + request: ChatCompletionRequestWithTokens, + raw_request: Optional[Request] = None, + ) -> Union[AsyncGenerator[str, None], ChatCompletionResponse, ErrorResponse]: + """ + Chat Completion API similar to OpenAI's API. + + See https://platform.openai.com/docs/api-reference/chat/create + for the API specification. This API mimics the OpenAI + Chat Completion API. + """ + # Streaming response + tokenizer = self.renderer.tokenizer + assert tokenizer is not None + reasoning_parser: ReasoningParser | None = None + try: + if self.reasoning_parser_cls: + # Pass the same chat template kwargs as used in tokenization + chat_template_kwargs = self._prepare_extra_chat_template_kwargs( + request.chat_template_kwargs, + self.default_chat_template_kwargs, + ) + reasoning_parser = self.reasoning_parser_cls( + tokenizer, + chat_template_kwargs=chat_template_kwargs, # type: ignore[call-arg] + ) + except RuntimeError as e: + logger.exception("Error in reasoning parser creation.") + return self.create_error_response(str(e)) + result = await self.render_chat_request(request) + if isinstance(result, ErrorResponse): + return result + + conversation, engine_prompts = result + + # We override prompt tokens directly. + # VLM conversations use MITO (message-based) instead of TITO, so + # multi_modal_data is not expected here. + engine_prompts[0]["prompt_token_ids"] = request.tokens # type: ignore + + request_id = f"chatcmpl-{self._base_request_id(raw_request, request.request_id)}" + + request_metadata = RequestResponseMetadata(request_id=request_id) + if raw_request: + raw_request.state.request_metadata = request_metadata + + try: + lora_request = self._maybe_get_adapters(request, supports_default_mm_loras=True) + + model_name = self.models.model_name(lora_request) + except (ValueError, TypeError, RuntimeError) as e: + logger.exception("Error preparing request components") + return self.create_error_response(e) + + # Extract data_parallel_rank from header (router can inject it) + data_parallel_rank = self._get_data_parallel_rank(raw_request) + + # Schedule the request and get the result generator. + max_model_len = self.model_config.max_model_len + generators: list[AsyncGenerator[RequestOutput, None]] = [] + try: + for i, engine_prompt in enumerate(engine_prompts): + prompt_token_ids = self._extract_prompt_components(engine_prompt).token_ids + + # If we are creating sub requests for multiple prompts, ensure that they + # have unique request ids. + sub_request_id = request_id if len(engine_prompts) == 1 else f"{request_id}_{i}" + + prompt_len = self._extract_prompt_len(engine_prompt) + if prompt_len >= max_model_len: + raise VLLMValidationError( + f"This model's maximum context length is " + f"{max_model_len} tokens. However, your request has " + f"{prompt_len} input tokens. Please reduce the length of " + "the input messages.", + parameter="input_tokens", + value=prompt_len, + ) + + max_tokens = get_max_tokens( + max_model_len, + request.max_completion_tokens if request.max_completion_tokens is not None else request.max_tokens, + self._extract_prompt_len(engine_prompt), + self.default_sampling_params, + self.override_max_tokens, + ) + + sampling_params: SamplingParams | BeamSearchParams + if request.use_beam_search: + sampling_params = request.to_beam_search_params(max_tokens, self.default_sampling_params) + else: + sampling_params = request.to_sampling_params( + max_tokens, + self.default_sampling_params, + ) + + self._log_inputs( + sub_request_id, + engine_prompt, + params=sampling_params, + lora_request=lora_request, + ) + + trace_headers = None if raw_request is None else await self._get_trace_headers(raw_request.headers) + + if isinstance(sampling_params, BeamSearchParams): + generator = self.beam_search( + prompt=engine_prompt, + request_id=sub_request_id, + params=sampling_params, + lora_request=lora_request, + trace_headers=trace_headers, + ) + else: + reasoning_ended = ( + reasoning_parser.is_reasoning_end(prompt_token_ids or []) if reasoning_parser else None + ) + + generator = self.engine_client.generate( + engine_prompt, + sampling_params, + sub_request_id, + lora_request=lora_request, + trace_headers=trace_headers, + priority=request.priority, + data_parallel_rank=data_parallel_rank, + reasoning_ended=reasoning_ended, + ) + + generators.append(generator) + except ValueError as e: + return self.create_error_response(e) + + assert len(generators) == 1 + (result_generator,) = generators + + if request.stream: + return self.chat_completion_stream_generator( + request, + result_generator, + request_id, + model_name, + conversation, + tokenizer, + request_metadata, + reasoning_parser, + ) + + try: + return await self.chat_completion_full_generator( + request, + result_generator, + request_id, + model_name, + conversation, + tokenizer, + request_metadata, + reasoning_parser, + ) + except GenerationError: + raise # Let FastAPI's global generation_error_handler handle it + except ValueError as e: + return self.create_error_response(e) From 515c48f0cbf2e719d0fcb72cbc5456001364acb8 Mon Sep 17 00:00:00 2001 From: hallerite Date: Wed, 29 Apr 2026 04:29:22 +0530 Subject: [PATCH 46/54] chore(orchestrator): cleanup-review fixes + bump verifiers - Drop dead is_vlm parameter from setup_rollout_inference_pool. Its only use was a defensive assert that the config validator (validate_renderer_vs_vlm) already enforces. - Drop unused processor parameter from interleave_rollout (passed but never referenced inside the function). - Fix test_elastic_clients_preserve_renderer_model_name_when_model_name_updates: the MagicMock didn't initialize extra_headers_from_state to a dict, so pydantic rejected the resulting ClientConfig. - Bump verifiers pin to 69c2b4c (3 cleanup commits on the verifiers side: small fixes, drop extension-break diagnostic path, drop synthesize_close_on_truncation flag + strip multimodal). Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- src/prime_rl/orchestrator/orchestrator.py | 6 ----- src/prime_rl/orchestrator/trajectories.py | 1 - tests/unit/utils/test_elastic.py | 1 + uv.lock | 29 ++++++++++++++++------- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ac755937621..e9b7137e5f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,7 +125,7 @@ override-dependencies = [ [tool.uv.sources] torch = { index = "pytorch-cu128" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "17358ee" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "69c2b4c" } torchtitan = { git = "https://github.com/pytorch/torchtitan", rev = "a1fdd7e" } dion = { git = "https://github.com/samsja/dion.git", rev = "d891eeb" } transformers = { git = "https://github.com/huggingface/transformers.git", rev = "c1c3424" } diff --git a/src/prime_rl/orchestrator/orchestrator.py b/src/prime_rl/orchestrator/orchestrator.py index 2c4e9d3829f..664a62298a9 100644 --- a/src/prime_rl/orchestrator/orchestrator.py +++ b/src/prime_rl/orchestrator/orchestrator.py @@ -152,7 +152,6 @@ async def orchestrate(config: OrchestratorConfig): rollout_client_config=rollout_client_config, rollout_model_name=rollout_model_name, tokenizer=tokenizer, - is_vlm=is_vlm, logger=logger, ) @@ -528,7 +527,6 @@ def process_rollout(rollout: vf.RolloutOutput, rollout_idx: int) -> list[Trainin vlm_cache=vlm_cache, cache_key=rollout_idx, mm_token_type_ids_mapping=mm_token_type_ids_mapping, - processor=processor, ) results = await asyncio.gather( @@ -875,7 +873,6 @@ async def setup_rollout_inference_pool( rollout_client_config, rollout_model_name: str, tokenizer, - is_vlm: bool, logger, ): """Set up rollout inference. @@ -905,9 +902,6 @@ async def setup_rollout_inference_pool( return None, inference_pool if config.use_renderer: - # Config validator already blocks use_renderer=True for VLMs; the - # is_vlm arg is kept for explicitness. - assert not is_vlm, "config validator should have rejected use_renderer=True for a VLM" renderer = create_renderer( tokenizer, renderer=config.model.renderer, diff --git a/src/prime_rl/orchestrator/trajectories.py b/src/prime_rl/orchestrator/trajectories.py index 9a2657ce0a1..3a45ee9ada5 100644 --- a/src/prime_rl/orchestrator/trajectories.py +++ b/src/prime_rl/orchestrator/trajectories.py @@ -254,7 +254,6 @@ def interleave_rollout( vlm_cache: "VLMImageCache | None" = None, cache_key: int | None = None, mm_token_type_ids_mapping: dict[int, int] | None = None, - processor: Any | None = None, ) -> list[TrainingSample] | None: """ Convert vf.RolloutOutput to trainable rollouts by interleaving trajectory steps diff --git a/tests/unit/utils/test_elastic.py b/tests/unit/utils/test_elastic.py index 9e08e0f7519..a2cd40d1d17 100644 --- a/tests/unit/utils/test_elastic.py +++ b/tests/unit/utils/test_elastic.py @@ -415,6 +415,7 @@ def test_elastic_clients_preserve_renderer_model_name_when_model_name_updates(): client_config.connect_timeout = 30.0 client_config.api_key_var = "PRIME_API_KEY" client_config.headers = {} + client_config.extra_headers_from_state = {} client_config.dp_rank_count = 1 pool = ElasticInferencePool( diff --git a/uv.lock b/uv.lock index 62cda69e605..11a1951e454 100644 --- a/uv.lock +++ b/uv.lock @@ -2306,17 +2306,17 @@ wheels = [ [[package]] name = "openai-harmony" -version = "0.0.4" +version = "0.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/94/01509d510bebf6606614e51113e5a415ced15b8f34aa98a8bf2539314650/openai_harmony-0.0.4.tar.gz", hash = "sha256:5c67ac6df349236fb7b64f57c3dbb0273efcdca24314daa108f2a482c427106c", size = 279848, upload-time = "2025-08-09T01:43:24.974Z" } +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/05/88/ade63bd8f36603610040e7cc086bc134d57a99a742e05f7fcddfdf822ee1/openai_harmony-0.0.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf2344366f10981bbc0f6d9949a0b2bb87151d209ed295943ed6ad8eda37932", size = 2963206, upload-time = "2025-08-09T01:43:02.433Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ec/dcdcace0ffcf3a532cca910e0c351b62d3a7decf0b091ea8cf856d2a67a6/openai_harmony-0.0.4-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31e9bcac0902a309e2fc688e52f247eec7fffcd00d17e958b9a83a8fea6519c2", size = 3049306, upload-time = "2025-08-09T01:43:11.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/39/172f1048d935db1523a82b45fee5231ad6c622645e566706e6bcf3731da8/openai_harmony-0.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:96a63199c0d81095b5d5d1ae8ca82b64c1c13d18d4e30323ae9e8ab31bc80a3d", size = 3121347, upload-time = "2025-08-09T01:43:16.705Z" }, - { url = "https://files.pythonhosted.org/packages/a8/bd/aa9e6e5cf140716dbcae17402fac2a81a9ebb3f934059ac0eec61cb447fc/openai_harmony-0.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:15e6d53a66502491a3675a536df30e271f976e6c5efe68250a65191efcb85c4f", size = 3221129, upload-time = "2025-08-09T01:43:23.146Z" }, + { 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/25/3f/1a192b93bb47c6b44cd98ba8cc1d3d2a9308f1bb700c3017e6352da11bda/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c007d277218a50db8839e599ed78e0fffe5130f614c3f6d93ae257f282071a29", size = 2953260, upload-time = "2025-11-05T19:06:55.406Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/93b582cad3531797c3db7c2db5400fd841538ccddfd9f5e3df61be99a630/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8565d4f5a0638da1bffde29832ed63c9e695c558611053add3b2dc0b56c92dbc", size = 3127044, upload-time = "2025-11-05T19:06:59.553Z" }, + { url = "https://files.pythonhosted.org/packages/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" }, ] [[package]] @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=980a9ea" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=69c2b4c" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3246,6 +3246,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/f4/04ed04ebf335a44083695c22772be6a42efa31900415555563acf02cb4de/regex-2026.3.32-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b56993a7aeb4140c4770f4f7965c9e5af4f024457d06e23c01b0d47501cb18ed", size = 788332, upload-time = "2026-03-28T21:46:50.454Z" }, ] +[[package]] +name = "renderers" +version = "0.1.0" +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=69c2b4c#69c2b4c95ce5221e9e74735aea4272e21d552747" } +dependencies = [ + { name = "openai-harmony", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -4035,13 +4044,14 @@ wheels = [ [[package]] name = "verifiers" -version = "0.1.13.dev8" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=980a9ea#980a9ea855cdac9e06426e0434c77e35ad27b3d5" } +version = "0.1.13.dev7" +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=69c2b4c#69c2b4c95ce5221e9e74735aea4272e21d552747" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "datasets", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "gepa", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "httpx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "jinja2", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "math-verify", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "mcp", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -4055,6 +4065,7 @@ dependencies = [ { name = "pydantic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "pyzmq", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "regex", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "renderers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "requests", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "rich", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "setproctitle", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From c61d8423f6f4054fe9e177325cc1344627002183 Mon Sep 17 00:00:00 2001 From: hallerite Date: Wed, 29 Apr 2026 17:17:21 +0530 Subject: [PATCH 47/54] chore(deps): bump verifiers to ee6cdb5 Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e9b7137e5f9..ea530080ecf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,7 +125,7 @@ override-dependencies = [ [tool.uv.sources] torch = { index = "pytorch-cu128" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "69c2b4c" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "ee6cdb5" } torchtitan = { git = "https://github.com/pytorch/torchtitan", rev = "a1fdd7e" } dion = { git = "https://github.com/samsja/dion.git", rev = "d891eeb" } transformers = { git = "https://github.com/huggingface/transformers.git", rev = "c1c3424" } diff --git a/uv.lock b/uv.lock index 11a1951e454..dc683100154 100644 --- a/uv.lock +++ b/uv.lock @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=69c2b4c" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=ee6cdb5" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3249,7 +3249,7 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=69c2b4c#69c2b4c95ce5221e9e74735aea4272e21d552747" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=ee6cdb5#ee6cdb5e2fd3a8e0e6b4ed0fb219085c44567062" } dependencies = [ { name = "openai-harmony", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -4045,7 +4045,7 @@ wheels = [ [[package]] name = "verifiers" version = "0.1.13.dev7" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=69c2b4c#69c2b4c95ce5221e9e74735aea4272e21d552747" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=ee6cdb5#ee6cdb5e2fd3a8e0e6b4ed0fb219085c44567062" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From 519f61e932d679e3cebbc2af0dcdb930ad1faf01 Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 30 Apr 2026 03:41:54 +0530 Subject: [PATCH 48/54] chore(deps): bump verifiers to b7dd85f (renderers merged to main) Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ea530080ecf..2399fa4fdc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,7 +125,7 @@ override-dependencies = [ [tool.uv.sources] torch = { index = "pytorch-cu128" } -verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "ee6cdb5" } +verifiers = { git = "https://github.com/PrimeIntellect-ai/verifiers.git", rev = "b7dd85f" } torchtitan = { git = "https://github.com/pytorch/torchtitan", rev = "a1fdd7e" } dion = { git = "https://github.com/samsja/dion.git", rev = "d891eeb" } transformers = { git = "https://github.com/huggingface/transformers.git", rev = "c1c3424" } diff --git a/uv.lock b/uv.lock index dc683100154..62e25bd6215 100644 --- a/uv.lock +++ b/uv.lock @@ -2798,7 +2798,7 @@ requires-dist = [ { name = "torchtitan", git = "https://github.com/pytorch/torchtitan?rev=a1fdd7e" }, { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=c1c3424" }, { name = "uvloop", specifier = ">=0.21.0" }, - { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=ee6cdb5" }, + { name = "verifiers", git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=b7dd85f" }, { name = "vllm", specifier = ">=0.19.0" }, { name = "vllm-router", marker = "platform_machine == 'x86_64' and extra == 'disagg'", url = "https://github.com/PrimeIntellect-ai/router/releases/download/v0.1.22/vllm_router-0.1.22-cp38-abi3-manylinux_2_28_x86_64.whl" }, { name = "wandb", specifier = ">=0.24.2" }, @@ -3249,7 +3249,7 @@ wheels = [ [[package]] name = "renderers" version = "0.1.0" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=ee6cdb5#ee6cdb5e2fd3a8e0e6b4ed0fb219085c44567062" } +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?subdirectory=packages%2Frenderers&rev=b7dd85f#b7dd85fbde651b71e25d8f425db03407eda7884e" } dependencies = [ { name = "openai-harmony", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "transformers", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -4044,8 +4044,8 @@ wheels = [ [[package]] name = "verifiers" -version = "0.1.13.dev7" -source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=ee6cdb5#ee6cdb5e2fd3a8e0e6b4ed0fb219085c44567062" } +version = "0.1.13.dev8" +source = { git = "https://github.com/PrimeIntellect-ai/verifiers.git?rev=b7dd85f#b7dd85fbde651b71e25d8f425db03407eda7884e" } dependencies = [ { name = "aiolimiter", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "anthropic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From 76c8590b268937b8b4432f0cda2bd972e3c8d845 Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 30 Apr 2026 03:44:26 +0530 Subject: [PATCH 49/54] test(orchestrator): set use_renderer/tool_parser/reasoning_parser/renderer_pool_size on mock The renderer-client setup test built a SimpleNamespace mock missing attributes the production code reads (config.use_renderer, config.model.tool_parser, config.model.reasoning_parser, config.model.renderer_pool_size), so the test crashed with AttributeError before exercising the renderer path. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../orchestrator/test_orchestrator_setup.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/unit/orchestrator/test_orchestrator_setup.py b/tests/unit/orchestrator/test_orchestrator_setup.py index 4c96878686c..1645e20d5c5 100644 --- a/tests/unit/orchestrator/test_orchestrator_setup.py +++ b/tests/unit/orchestrator/test_orchestrator_setup.py @@ -42,7 +42,15 @@ async def run() -> None: tokenizer = object() config = SimpleNamespace( teacher_rollout_model=None, - model=SimpleNamespace(renderer="qwen3_vl", name="student-model"), + use_renderer=True, + use_token_client=False, + model=SimpleNamespace( + renderer="qwen3_vl", + name="student-model", + tool_parser=None, + reasoning_parser=None, + renderer_pool_size=None, + ), ) rollout_client_config = SimpleNamespace(base_url=["http://localhost:8000/v1"]) logger = MagicMock() @@ -66,13 +74,21 @@ async def run() -> None: assert returned_renderer is renderer assert returned_pool is inference_pool - create_renderer_mock.assert_called_once_with(tokenizer, renderer="qwen3_vl") + create_renderer_mock.assert_called_once_with( + tokenizer, + renderer="qwen3_vl", + tool_parser=None, + reasoning_parser=None, + ) setup_pool_mock.assert_awaited_once_with( rollout_client_config, model_name="student-model", train_client_type="renderer", eval_client_type="openai_chat_completions", renderer_name="qwen3_vl", + tool_parser=None, + reasoning_parser=None, + renderer_pool_size=None, ) asyncio.run(run()) From 540500dde860d9aa7caf485bcf0d0c29da8bcad0 Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 30 Apr 2026 03:46:47 +0530 Subject: [PATCH 50/54] revert: restore docs/on_policy_distillation.md and skills/entrypoints/SKILL.md The renderer-client work removed `use_token_client = false` from the hard-distill example and replaced the `/v1/chat/completions/tokens` (TITO) endpoint reference in the entrypoints skill with `/v1/generate`. TITO is still served alongside `/v1/generate` on this branch, so the hard-distill documentation belongs as it was. The other unrelated notes added to the entrypoints skill don't belong there either. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/on_policy_distillation.md | 1 + skills/entrypoints/SKILL.md | 16 +--------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/docs/on_policy_distillation.md b/docs/on_policy_distillation.md index 10699c520e5..093ccfec710 100644 --- a/docs/on_policy_distillation.md +++ b/docs/on_policy_distillation.md @@ -71,6 +71,7 @@ Use this mode when you want to train from teacher-generated completions directly type = "sft" [orchestrator] +use_token_client = false [orchestrator.teacher_rollout_model.client] base_url = ["https://your-openai-compatible-endpoint/v1"] diff --git a/skills/entrypoints/SKILL.md b/skills/entrypoints/SKILL.md index bf0888b7b30..f6589aca976 100644 --- a/skills/entrypoints/SKILL.md +++ b/skills/entrypoints/SKILL.md @@ -17,20 +17,6 @@ uv run rl @ examples/reverse_text/rl.toml @ examples/reverse_text/slurm_rl.toml uv run rl @ examples/reverse_text/rl.toml --dry-run # generate scripts without running ``` -When running from a fresh worktree, include optional extras required by the config. For example, VLM color-codeword debugging needs FlashAttention and the env packages: - -```bash -uv run --extra flash-attn --extra envs rl @ configs/multimodal/rl_color_codeword_test.toml -``` - -Short debug runs can write large filesystem weight-broadcast snapshots. If `/` is low on space, put the debug `--output-dir` on `/dev/shm` and clean it up afterward. - -For hard-distillation with `orchestrator.teacher_rollout_model`, omit `[inference]`. That mode talks directly to the external OpenAI-compatible rollout endpoint and reconstructs training tokens from messages; it does not use the local direct renderer client or `/v1/generate`. - -For LoRA RL smoke runs against local vLLM, set `VLLM_ALLOW_RUNTIME_LORA_UPDATING=True` in the environment or adapter load/update requests will fail. - -For ad-hoc `CliAgentEnv`/`ComposableEnv` sandbox smoke tests that call back through Prime Tunnel using Python stdlib `urllib`, set an explicit non-default `User-Agent` header such as `curl/7.81.0`. The tunnel can reject Python's default `urllib` user agent from inside Prime sandboxes with HTTP 403 / code 1010. Prefer uploading an agent script into the sandbox and running `python3 /tmp/agent.py` over passing a heredoc directly as a `start_background_job` command. - - **Config:** `RLConfig` (`src/prime_rl/configs/rl.py`) - **Entrypoint:** `src/prime_rl/entrypoints/rl.py` - **SLURM:** yes — single-node and multi-node @@ -63,7 +49,7 @@ uv run inference --model.name Qwen/Qwen3-0.6B --model.enforce-eager Always use the `inference` entrypoint — never `vllm serve` directly. Custom endpoints beyond standard OpenAI API: -- `/v1/generate` — accepts token IDs as prompt input, with optional images +- `/v1/chat/completions/tokens` — accepts token IDs as prompt input - `/update_weights` — hot-reload model weights from the trainer - `/load_lora_adapter` — load LoRA adapters at runtime - `/init_broadcaster` — initialize weight broadcast for distributed training From 717c6e0fe666d0d262381c6965e578febaba338f Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 30 Apr 2026 05:19:21 +0530 Subject: [PATCH 51/54] address reviews Co-Authored-By: Claude Opus 4.7 (1M context) --- src/prime_rl/configs/orchestrator.py | 29 ++++++++++--------- src/prime_rl/configs/rl.py | 18 ------------ src/prime_rl/configs/shared.py | 28 ++++++++++++------ src/prime_rl/orchestrator/orchestrator.py | 14 ++++----- src/prime_rl/utils/client.py | 4 +-- src/prime_rl/utils/elastic.py | 4 +-- .../orchestrator/test_orchestrator_setup.py | 8 ++--- 7 files changed, 50 insertions(+), 55 deletions(-) diff --git a/src/prime_rl/configs/orchestrator.py b/src/prime_rl/configs/orchestrator.py index 4e73efd68fc..922ae0d464a 100644 --- a/src/prime_rl/configs/orchestrator.py +++ b/src/prime_rl/configs/orchestrator.py @@ -11,6 +11,7 @@ HeartbeatConfig, LogConfig, PrimeMonitorConfig, + RendererConfig, TransportConfig, WandbWithExtrasConfig, ) @@ -870,6 +871,9 @@ class OrchestratorConfig(BaseConfig): # The tokenizer configuration tokenizer: TokenizerConfig = TokenizerConfig() + # The renderer configuration (only used when use_renderer=True) + renderer: RendererConfig = RendererConfig() + # The optimizer configuration (per-run LR for multi-run training) optim: OptimizerConfig = OptimizerConfig() @@ -1069,10 +1073,9 @@ class OrchestratorConfig(BaseConfig): Field( description="Whether to use the renderer client (client-side tokenization via the ``renderers`` package, " "served by ``/v1/generate``). Mutually exclusive with ``use_token_client``. When True, the " - "``model.renderer`` / ``model.tool_parser`` / ``model.reasoning_parser`` / " - "``model.renderer_pool_size`` knobs apply; when False they must be left at their defaults. " - "Not supported for VLMs — VLMs must use the token client (TITO) so image preprocessing and chat " - "templating stay server-side." + "``[orchestrator.renderer]`` block (name / tool_parser / reasoning_parser / pool_size) " + "applies; when False those fields must be left at their defaults. Not supported for VLMs — " + "VLMs must use the token client (TITO) so image preprocessing and chat templating stay server-side." ), ] = False @@ -1187,21 +1190,21 @@ def validate_renderer_vs_vlm(self): @model_validator(mode="after") def validate_renderer_args(self): - """Renderer-specific knobs on ``model`` are only meaningful when + """``[orchestrator.renderer]`` knobs are only meaningful when ``use_renderer=True``. Reject otherwise so callers don't silently pass them and wonder why they're ignored.""" if self.use_renderer: return self renderer_args_set = [] - if self.model.renderer != "auto": - renderer_args_set.append(f"model.renderer={self.model.renderer!r}") - if self.model.tool_parser is not None: - renderer_args_set.append(f"model.tool_parser={self.model.tool_parser!r}") - if self.model.reasoning_parser is not None: - renderer_args_set.append(f"model.reasoning_parser={self.model.reasoning_parser!r}") - if self.model.renderer_pool_size is not None: - renderer_args_set.append(f"model.renderer_pool_size={self.model.renderer_pool_size!r}") + if self.renderer.name != "auto": + renderer_args_set.append(f"renderer.name={self.renderer.name!r}") + if self.renderer.tool_parser is not None: + renderer_args_set.append(f"renderer.tool_parser={self.renderer.tool_parser!r}") + if self.renderer.reasoning_parser is not None: + renderer_args_set.append(f"renderer.reasoning_parser={self.renderer.reasoning_parser!r}") + if self.renderer.pool_size is not None: + renderer_args_set.append(f"renderer.pool_size={self.renderer.pool_size!r}") if renderer_args_set: raise ValueError( diff --git a/src/prime_rl/configs/rl.py b/src/prime_rl/configs/rl.py index 8c6438e88ff..6b0d609feb9 100644 --- a/src/prime_rl/configs/rl.py +++ b/src/prime_rl/configs/rl.py @@ -589,24 +589,6 @@ def auto_setup_model(self): if self.inference is not None: self.inference.model.vlm = self.model.vlm - if self.model.tool_parser is not None: - self.trainer.model.tool_parser = self.model.tool_parser - self.orchestrator.model.tool_parser = self.model.tool_parser - if self.inference is not None: - self.inference.model.tool_parser = self.model.tool_parser - - if self.model.reasoning_parser is not None: - self.trainer.model.reasoning_parser = self.model.reasoning_parser - self.orchestrator.model.reasoning_parser = self.model.reasoning_parser - if self.inference is not None: - self.inference.model.reasoning_parser = self.model.reasoning_parser - - if self.model.renderer_pool_size is not None: - self.trainer.model.renderer_pool_size = self.model.renderer_pool_size - self.orchestrator.model.renderer_pool_size = self.model.renderer_pool_size - if self.inference is not None: - self.inference.model.renderer_pool_size = self.model.renderer_pool_size - validate_shared_model_name(self.trainer, self.orchestrator, self.inference) return self diff --git a/src/prime_rl/configs/shared.py b/src/prime_rl/configs/shared.py index c3456a7e182..d26c33d9a97 100644 --- a/src/prime_rl/configs/shared.py +++ b/src/prime_rl/configs/shared.py @@ -124,7 +124,24 @@ class BaseModelConfig(BaseConfig): ), ] = False - renderer: Annotated[ + vlm: Annotated[ + "VLMConfig | None", + Field( + description="VLM configuration. Set this to enable vision-language model support.", + ), + ] = None + + +class RendererConfig(BaseConfig): + """Configures the client-side renderer (chat-template + response parsing). + + Only consumed when ``orchestrator.use_renderer = true``. The renderer + owns both directions: render messages → token ids on the client, and + parse model output tokens → structured ``content`` / ``reasoning_content`` + / ``tool_calls``. + """ + + name: Annotated[ str, Field( description=( @@ -157,7 +174,7 @@ class BaseModelConfig(BaseConfig): ), ] = None - renderer_pool_size: Annotated[ + pool_size: Annotated[ int | None, Field( ge=1, @@ -169,13 +186,6 @@ class BaseModelConfig(BaseConfig): ), ] = None - vlm: Annotated[ - "VLMConfig | None", - Field( - description="VLM configuration. Set this to enable vision-language model support.", - ), - ] = None - class ElasticConfig(BaseConfig): """Configures elastic inference pool with DNS-based service discovery. diff --git a/src/prime_rl/orchestrator/orchestrator.py b/src/prime_rl/orchestrator/orchestrator.py index 664a62298a9..e83d848b931 100644 --- a/src/prime_rl/orchestrator/orchestrator.py +++ b/src/prime_rl/orchestrator/orchestrator.py @@ -904,9 +904,9 @@ async def setup_rollout_inference_pool( if config.use_renderer: renderer = create_renderer( tokenizer, - renderer=config.model.renderer, - tool_parser=config.model.tool_parser, - reasoning_parser=config.model.reasoning_parser, + renderer=config.renderer.name, + tool_parser=config.renderer.tool_parser, + reasoning_parser=config.renderer.reasoning_parser, ) logger.info(f"Initialized {type(renderer).__name__} for {config.model.name}") inference_pool = await setup_inference_pool( @@ -914,10 +914,10 @@ async def setup_rollout_inference_pool( model_name=rollout_model_name, train_client_type="renderer", eval_client_type="openai_chat_completions", - renderer_name=config.model.renderer, - tool_parser=config.model.tool_parser, - reasoning_parser=config.model.reasoning_parser, - renderer_pool_size=config.model.renderer_pool_size, + renderer_name=config.renderer.name, + tool_parser=config.renderer.tool_parser, + reasoning_parser=config.renderer.reasoning_parser, + renderer_pool_size=config.renderer.pool_size, ) logger.info("Using direct renderer rollout client") return renderer, inference_pool diff --git a/src/prime_rl/utils/client.py b/src/prime_rl/utils/client.py index 13730729920..21659dfc46b 100644 --- a/src/prime_rl/utils/client.py +++ b/src/prime_rl/utils/client.py @@ -62,7 +62,7 @@ def __init__( self, client_config: ClientConfig, model_name: str, - train_client_type: str = "renderer", + train_client_type: str = "openai_chat_completions_token", eval_client_type: str = "openai_chat_completions", renderer_name: str = "auto", tool_parser: str | None = None, @@ -123,7 +123,7 @@ async def stop(self) -> None: async def setup_inference_pool( client_config: ClientConfig, model_name: str, - train_client_type: str = "renderer", + train_client_type: str = "openai_chat_completions_token", eval_client_type: str = "openai_chat_completions", renderer_name: str = "auto", tool_parser: str | None = None, diff --git a/src/prime_rl/utils/elastic.py b/src/prime_rl/utils/elastic.py index f29ab8b903d..902f873903d 100644 --- a/src/prime_rl/utils/elastic.py +++ b/src/prime_rl/utils/elastic.py @@ -104,7 +104,7 @@ def __init__( self, client_config: ClientConfig, model_name: str, - train_client_type: str = "renderer", + train_client_type: str = "openai_chat_completions_token", eval_client_type: str = "openai_chat_completions", renderer_name: str = "auto", tool_parser: str | None = None, @@ -146,7 +146,7 @@ async def from_config( cls, client_config: ClientConfig, model_name: str, - train_client_type: str = "renderer", + train_client_type: str = "openai_chat_completions_token", eval_client_type: str = "openai_chat_completions", renderer_name: str = "auto", tool_parser: str | None = None, diff --git a/tests/unit/orchestrator/test_orchestrator_setup.py b/tests/unit/orchestrator/test_orchestrator_setup.py index 1645e20d5c5..ff9bb5b79f9 100644 --- a/tests/unit/orchestrator/test_orchestrator_setup.py +++ b/tests/unit/orchestrator/test_orchestrator_setup.py @@ -44,12 +44,12 @@ async def run() -> None: teacher_rollout_model=None, use_renderer=True, use_token_client=False, - model=SimpleNamespace( - renderer="qwen3_vl", - name="student-model", + model=SimpleNamespace(name="student-model"), + renderer=SimpleNamespace( + name="qwen3_vl", tool_parser=None, reasoning_parser=None, - renderer_pool_size=None, + pool_size=None, ), ) rollout_client_config = SimpleNamespace(base_url=["http://localhost:8000/v1"]) From 5b6f42bcaeef098814208539f6c1934f5adbbcc2 Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 30 Apr 2026 05:24:23 +0530 Subject: [PATCH 52/54] fix(config): drop orphan renderer fields from SharedModelConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Followup to the renderer-subconfig refactor: the prior commit moved fan-out logic out of rl.py but left the corresponding fields on SharedModelConfig in place. They had no effect — values written under top-level [model] were silently ignored. Drop them so users see a config-validation error and write [orchestrator.renderer] instead. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/prime_rl/configs/rl.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/prime_rl/configs/rl.py b/src/prime_rl/configs/rl.py index 6b0d609feb9..0b3e5e8030a 100644 --- a/src/prime_rl/configs/rl.py +++ b/src/prime_rl/configs/rl.py @@ -148,21 +148,6 @@ class SharedModelConfig(BaseConfig): Field(description="VLM configuration. Set to enable vision-language model support."), ] = None - tool_parser: Annotated[ - str | None, - Field(description="Tool parser name (propagates to orchestrator/trainer/inference model configs)."), - ] = None - - reasoning_parser: Annotated[ - str | None, - Field(description="Reasoning parser name (propagates to orchestrator/trainer/inference model configs)."), - ] = None - - renderer_pool_size: Annotated[ - int | None, - Field(ge=1, description="Renderer pool size (propagates to orchestrator/trainer/inference)."), - ] = None - class SharedWeightBroadcastConfig(BaseConfig): """Configures shared weight broadcast settings.""" From d5ea6361457f848682e20983e0651ea9bbd4771b Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 30 Apr 2026 05:31:25 +0530 Subject: [PATCH 53/54] fix(server): validate /v1/generate body via FastAPI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the handler did manual `body = await raw_request.json(); GenerateRequest(**body)` so JSON / pydantic errors bubbled up as 500. Switch to the typed-parameter + validate_json_request dependency pattern already used by /v1/chat/completions/tokens — malformed requests now return 422 with field-level errors. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/prime_rl/inference/vllm/server.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/prime_rl/inference/vllm/server.py b/src/prime_rl/inference/vllm/server.py index 6ea7ab848d4..a8920785ca9 100644 --- a/src/prime_rl/inference/vllm/server.py +++ b/src/prime_rl/inference/vllm/server.py @@ -19,6 +19,8 @@ from vllm.logger import init_logger from vllm.utils.argparse_utils import FlexibleArgumentParser +from prime_rl.inference.vllm.serving_generate import GenerateRequest + from prime_rl.configs.inference import InferenceConfig from prime_rl.utils.logger import get_logger @@ -186,14 +188,19 @@ def chat_with_tokens(request: Request) -> OpenAIServingChatWithTokens | None: return request.app.state.openai_serving_chat_with_tokens -@router.post("/v1/generate") +@router.post( + "/v1/generate", + dependencies=[Depends(validate_json_request)], + responses={ + HTTPStatus.OK.value: {"content": {"application/json": {}}}, + HTTPStatus.BAD_REQUEST.value: {"model": ErrorResponse}, + HTTPStatus.NOT_FOUND.value: {"model": ErrorResponse}, + HTTPStatus.INTERNAL_SERVER_ERROR.value: {"model": ErrorResponse}, + }, +) @with_cancellation @load_aware_call -async def _generate(raw_request: Request): - from prime_rl.inference.vllm.serving_generate import GenerateRequest - - body = await raw_request.json() - request = GenerateRequest(**body) +async def _generate(request: GenerateRequest, raw_request: Request): handler = generate_handler(raw_request) if handler is None: return JSONResponse({"error": "generate endpoint not available"}, status_code=503) From c97ecfd4299d8f40ca576f8fc878ae2e407f43f4 Mon Sep 17 00:00:00 2001 From: hallerite Date: Thu, 30 Apr 2026 05:32:34 +0530 Subject: [PATCH 54/54] style: ruff isort Co-Authored-By: Claude Opus 4.7 (1M context) --- src/prime_rl/inference/vllm/server.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/prime_rl/inference/vllm/server.py b/src/prime_rl/inference/vllm/server.py index a8920785ca9..68ce3182227 100644 --- a/src/prime_rl/inference/vllm/server.py +++ b/src/prime_rl/inference/vllm/server.py @@ -19,9 +19,8 @@ from vllm.logger import init_logger from vllm.utils.argparse_utils import FlexibleArgumentParser -from prime_rl.inference.vllm.serving_generate import GenerateRequest - from prime_rl.configs.inference import InferenceConfig +from prime_rl.inference.vllm.serving_generate import GenerateRequest from prime_rl.utils.logger import get_logger MODEL_TOOL_CALL_PARSER: dict[str, str] = {