Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ skyrl-train = [
"polars",
"s3fs",
"fastapi",
"orjson>=3.11.9",
"pybase64>=1.4.2",
"uvicorn",
"vllm-router; sys_platform == 'linux'",
"pybind11",
Expand Down
4 changes: 3 additions & 1 deletion skyrl/backends/skyrl_train/inference_servers/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Dict, Hashable, List, Optional, Tuple, TypedDict

from skyrl.utils.routed_experts import RoutedExpertIndices

if TYPE_CHECKING:
from skyrl.backends.skyrl_train.weight_sync import WeightUpdateRequest
from skyrl.backends.skyrl_train.weight_sync.transfer_strategy import (
Expand Down Expand Up @@ -47,7 +49,7 @@ class InferenceEngineOutput(TypedDict):
stop_reasons: List[str]
response_logprobs: Optional[List[List[float]]]
prompt_logprobs: Optional[List[List[float]]] # per-prompt-token logprobs under the current model
rollout_expert_indices: Optional[List[List[List[int]]]] # [seq_len, layer_num, topk]
rollout_expert_indices: Optional[List[RoutedExpertIndices]]


class InferenceEngineInterface(ABC):
Expand Down
292 changes: 168 additions & 124 deletions skyrl/backends/skyrl_train/inference_servers/remote_inference_client.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Compact routed-expert HTTP payloads."""

import math
from typing import Any

import numpy as np
import pybase64

from skyrl.utils.routed_experts import (
ROUTED_EXPERT_DTYPES,
RoutedExpertIndices,
compact_routed_expert_indices,
)

_DTYPES = {dtype.name: dtype for dtype in ROUTED_EXPERT_DTYPES}


def pack_routed_experts(routed_experts: RoutedExpertIndices) -> dict[str, Any]:
compact = compact_routed_expert_indices(routed_experts)
return {
"data": pybase64.b64encode(memoryview(compact)).decode("ascii"),
"shape": list(compact.shape),
"dtype": compact.dtype.name,
}


def decode_packed_routed_experts(payload: dict[str, Any]) -> RoutedExpertIndices:
if not isinstance(payload, dict):
raise TypeError("packed routed expert indices must be an object")
try:
dtype = _DTYPES[payload["dtype"]]
shape = tuple(payload["shape"])
data = pybase64.b64decode_as_bytearray(payload["data"], validate=True)
except (KeyError, TypeError, ValueError) as exc:
raise ValueError("invalid packed routed_experts payload") from exc
if len(shape) != 3 or any(type(dim) is not int or dim < 0 for dim in shape):
raise ValueError(f"invalid packed routed_experts shape: {shape}")
expected_size = math.prod(shape) * dtype.itemsize
if len(data) != expected_size:
raise ValueError(f"packed routed_experts has {len(data)} bytes, expected {expected_size}")
decoded = np.frombuffer(data, dtype=dtype).reshape(shape)
compact = compact_routed_expert_indices(decoded)
if compact.dtype != dtype:
raise ValueError(f"packed routed_experts uses non-canonical dtype {dtype.name}; expected {compact.dtype.name}")
return compact
21 changes: 14 additions & 7 deletions skyrl/backends/skyrl_train/inference_servers/vllm_server_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@

import asyncio
import logging
import math
import os
import time
from argparse import Namespace
from typing import List, Optional, Tuple

import httpx
import numpy as np
import orjson
import uvicorn
import vllm.envs as envs
from fastapi import HTTPException, Request
from fastapi import HTTPException, Request, Response
from ray.util.placement_group import PlacementGroup
from vllm.engine.arg_utils import AsyncEngineArgs
from vllm.engine.async_llm_engine import AsyncLLMEngine
Expand All @@ -34,6 +37,9 @@
get_node_ip,
)
from skyrl.backends.skyrl_train.inference_servers.protocols import ServerActorProtocol
from skyrl.backends.skyrl_train.inference_servers.routed_experts_wire import (
pack_routed_experts,
)
from skyrl.env_vars import (
SKYRL_HTTP_CONNECTION_LIMIT,
SKYRL_VLLM_DP_PORT_OFFSET,
Expand Down Expand Up @@ -424,20 +430,20 @@ async def _skyrl_generate(request: Request):
content = []
for tid, lp_dict in zip(token_ids_out, resp.logprobs):
if lp_dict and tid in lp_dict:
content.append({"logprob": lp_dict[tid].logprob})
logprob = lp_dict[tid].logprob
if not math.isfinite(logprob):
raise ValueError("Out of range float values are not JSON compliant")
content.append({"logprob": logprob})
Comment on lines +433 to +436

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Raising a ValueError when a logprob is not finite (e.g., -inf) will crash the entire generation request and fail the training run. It is much safer to fall back to a compliant default value like -9999.0 (which is already used as the default for missing logprobs) to prevent unexpected crashes.

Suggested change
logprob = lp_dict[tid].logprob
if not math.isfinite(logprob):
raise ValueError("Out of range float values are not JSON compliant")
content.append({"logprob": logprob})
logprob = lp_dict[tid].logprob
if not math.isfinite(logprob):
logprob = -9999.0
content.append({"logprob": logprob})

else:
# -9999.0 is the default in vLLM's ChatCompletionLogProb
content.append({"logprob": -9999.0})
logprobs = {"content": content}

routed_experts = None
if resp.routed_experts is not None:
if hasattr(resp.routed_experts, "tolist"):
routed_experts = resp.routed_experts.tolist()
else:
routed_experts = resp.routed_experts
routed_experts = pack_routed_experts(np.asarray(resp.routed_experts))

return {
payload = {
"choices": [
{
"token_ids": token_ids_out,
Expand All @@ -447,6 +453,7 @@ async def _skyrl_generate(request: Request):
}
]
}
return Response(content=orjson.dumps(payload), media_type="application/json")

async def shutdown(self) -> None:
"""Gracefully shutdown the server."""
Expand Down
17 changes: 16 additions & 1 deletion skyrl/backends/skyrl_train/training_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

import numpy as np
import torch
from jaxtyping import Float, Integer
from jaxtyping import Bool, Float, Integer

from skyrl.utils.routed_experts import make_replay_padding_indices

DictType = TypeVar("DictType")

Expand Down Expand Up @@ -476,6 +478,7 @@ class TrainingInput(TypedDict, total=False):
rewards: Optional[Float[torch.Tensor, "batch_size seq_len"]]
rollout_logprobs: Optional[Float[torch.Tensor, "batch_size seq_len"]]
rollout_expert_indices: Optional[Integer[torch.Tensor, "batch_size seq_len layer_num topk"]]
router_padding_mask: Optional[Bool[torch.Tensor, "batch_size seq_len"]]
pixel_values: Optional[TensorList] # list of `batch_size` [num_patches_i, dim] tensors
image_grid_thw: Optional[TensorList] # list of `batch_size` [num_images_i, 3] tensors

Expand Down Expand Up @@ -524,6 +527,18 @@ def pad_training_input_batch(unpadded_batch: TrainingInputBatch, pad_size: int)
additional_dims = tensor.shape[1:]
padding_tensor = torch.zeros(pad_size, *additional_dims, dtype=tensor.dtype, device=tensor.device)
new_tensors[key] = torch.cat([tensor, padding_tensor], dim=0)
elif key == "rollout_expert_indices":
additional_dims = tensor.shape[1:]
padding_tensor = make_replay_padding_indices(
(pad_size, *additional_dims),
dtype=tensor.dtype,
device=tensor.device,
)
new_tensors[key] = torch.cat([tensor, padding_tensor], dim=0)
elif key == "router_padding_mask":
additional_dims = tensor.shape[1:]
padding_tensor = torch.ones(pad_size, *additional_dims, dtype=torch.bool, device=tensor.device)
new_tensors[key] = torch.cat([tensor, padding_tensor], dim=0)
else:
# Copy row 0 `pad_size` times. Loss masked so values don't affect the loss. Just need valid shape/dtype.
assert tensor.shape[0] > 0, f"Cannot pad empty tensor field {key!r}"
Expand Down
Loading
Loading