Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

1244 lines
41 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Manual Pi0.5 SGLang vs OpenPI benchmark.
This script is intentionally outside the unit-test path. It needs GPU memory,
Pi0.5 checkpoints, and an OpenPI install for the baseline.
"""
from __future__ import annotations
import argparse
import asyncio
import dataclasses
import json
import sys
import time
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import numpy as np
import requests
SCRIPT_DIR = Path(__file__).resolve().parent
if sys.path and Path(sys.path[0]).resolve() == SCRIPT_DIR:
sys.path.pop(0)
from sglang.multimodal_gen.runtime.entrypoints.vla.protocol import ( # noqa: E402
pack_msgpack,
unpack_msgpack,
)
@dataclass(frozen=True)
class Pi05BenchProfile:
name: str
sglang_model: str
openpi_config: str
openpi_checkpoint: str
prompt: str
sglang_action_horizon: int
openpi_action_horizon: int
action_dim: int
output_action_dim: int
PROFILES = {
"libero": Pi05BenchProfile(
name="libero",
sglang_model="lerobot/pi05_libero_base",
openpi_config="pi05_libero",
openpi_checkpoint="gs://openpi-assets/checkpoints/pi05_libero",
prompt="pick up the object",
sglang_action_horizon=50,
openpi_action_horizon=10,
action_dim=32,
output_action_dim=7,
),
"aloha": Pi05BenchProfile(
name="aloha",
sglang_model="lerobot/pi05_base",
openpi_config="pi05_aloha",
openpi_checkpoint="gs://openpi-assets/checkpoints/pi05_base",
prompt="pick up the block",
sglang_action_horizon=50,
openpi_action_horizon=50,
action_dim=32,
output_action_dim=14,
),
}
def _stats_ms(samples: list[float]) -> dict[str, float]:
if not samples:
return {}
values = np.asarray(samples, dtype=np.float64)
return {
"count": float(len(samples)),
"mean_ms": float(np.mean(values)),
"std_ms": float(np.std(values)),
"p50_ms": float(np.quantile(values, 0.50)),
"p90_ms": float(np.quantile(values, 0.90)),
"p95_ms": float(np.quantile(values, 0.95)),
"min_ms": float(np.min(values)),
"max_ms": float(np.max(values)),
}
def _inc(mapping: dict[str, int], key: object, value: int) -> None:
key_str = str(key)
mapping[key_str] = mapping.get(key_str, 0) + value
def _summarize_torch_module(module) -> dict[str, Any]:
param_dtypes: dict[str, int] = {}
param_dtype_examples: dict[str, list[str]] = {}
buffer_dtypes: dict[str, int] = {}
buffer_dtype_examples: dict[str, list[str]] = {}
devices: dict[str, int] = {}
param_count = 0
buffer_count = 0
trainable_param_count = 0
for name, param in module.named_parameters(recurse=True):
numel = int(param.numel())
param_count += numel
if param.requires_grad:
trainable_param_count += numel
_inc(param_dtypes, param.dtype, numel)
_inc(devices, param.device, numel)
examples = param_dtype_examples.setdefault(str(param.dtype), [])
if len(examples) < 16:
examples.append(name)
for name, buffer in module.named_buffers(recurse=True):
numel = int(buffer.numel())
buffer_count += numel
_inc(buffer_dtypes, buffer.dtype, numel)
_inc(devices, buffer.device, numel)
examples = buffer_dtype_examples.setdefault(str(buffer.dtype), [])
if len(examples) < 16:
examples.append(name)
return {
"class": module.__class__.__name__,
"param_dtypes": param_dtypes,
"param_dtype_examples": param_dtype_examples,
"buffer_dtypes": buffer_dtypes,
"buffer_dtype_examples": buffer_dtype_examples,
"devices": devices,
"param_count": param_count,
"trainable_param_count": trainable_param_count,
"buffer_count": buffer_count,
}
def _torch_autocast_dtype(torch_module, device: str) -> str:
get_autocast_dtype = getattr(torch_module, "get_autocast_dtype", None)
if get_autocast_dtype is not None:
return str(get_autocast_dtype(device))
if device == "cuda":
return str(torch_module.get_autocast_gpu_dtype())
return str(torch_module.get_autocast_cpu_dtype())
def openpi_precision_metadata(policy) -> dict[str, Any]:
import torch
metadata: dict[str, Any] = {
"policy_class": policy.__class__.__name__,
"is_pytorch_model": bool(getattr(policy, "_is_pytorch_model", False)),
"pytorch_device": str(getattr(policy, "_pytorch_device", "")),
"torch_default_dtype": str(torch.get_default_dtype()),
"torch_autocast_cpu_dtype": _torch_autocast_dtype(torch, "cpu"),
}
if torch.cuda.is_available():
metadata["torch_autocast_cuda_dtype"] = _torch_autocast_dtype(torch, "cuda")
modules = {}
for name, value in vars(policy).items():
if isinstance(value, torch.nn.Module):
modules[name] = _summarize_torch_module(value)
metadata["torch_modules"] = modules
return metadata
def sglang_precision_metadata(pipeline) -> dict[str, Any]:
import torch
metadata: dict[str, Any] = {
"torch_default_dtype": str(torch.get_default_dtype()),
"torch_autocast_cpu_dtype": _torch_autocast_dtype(torch, "cpu"),
}
if torch.cuda.is_available():
metadata["torch_autocast_cuda_dtype"] = _torch_autocast_dtype(torch, "cuda")
modules = {}
policy_model = pipeline.get_module("policy_model")
if isinstance(policy_model, torch.nn.Module):
modules["policy_model"] = _summarize_torch_module(policy_model)
core_model = getattr(policy_model, "core_model", None)
if isinstance(core_model, torch.nn.Module):
modules["core_model"] = _summarize_torch_module(core_model)
metadata["torch_modules"] = modules
return metadata
def _image(rng: np.random.Generator, *, chw: bool = False) -> np.ndarray:
image = rng.integers(0, 256, size=(224, 224, 3), dtype=np.uint8)
if chw:
return np.transpose(image, (2, 0, 1))
return image
def _make_libero_observation(
rng: np.random.Generator,
prompt: str,
) -> tuple[dict[str, Any], dict[str, Any]]:
base_image = _image(rng)
wrist_image = _image(rng)
state = rng.random(8, dtype=np.float32)
openpi_obs = {
"observation/state": state,
"observation/image": base_image,
"observation/wrist_image": wrist_image,
"prompt": prompt,
}
sglang_observation = {
"images": {
"image": base_image,
"image2": wrist_image,
},
"state": state,
}
return openpi_obs, sglang_observation
def _make_aloha_observation(
rng: np.random.Generator,
prompt: str,
) -> tuple[dict[str, Any], dict[str, Any]]:
cam_high = _image(rng, chw=True)
cam_left = _image(rng, chw=True)
cam_right = _image(rng, chw=True)
state = np.ones((14,), dtype=np.float32)
openpi_obs = {
"state": state,
"images": {
"cam_high": cam_high,
"cam_low": _image(rng, chw=True),
"cam_left_wrist": cam_left,
"cam_right_wrist": cam_right,
},
"prompt": prompt,
}
sglang_state = np.zeros((32,), dtype=np.float32)
sglang_state[: state.shape[0]] = state
sglang_observation = {
"images": {
"base_0_rgb": np.transpose(cam_high, (1, 2, 0)),
"left_wrist_0_rgb": np.transpose(cam_left, (1, 2, 0)),
"right_wrist_0_rgb": np.transpose(cam_right, (1, 2, 0)),
},
"state": sglang_state,
}
return openpi_obs, sglang_observation
def build_observations(
profile: Pi05BenchProfile,
count: int,
seed: int,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
rng = np.random.default_rng(seed)
openpi_observations = []
sglang_observations = []
for _ in range(count):
if profile.name == "libero":
openpi_obs, sglang_obs = _make_libero_observation(rng, profile.prompt)
elif profile.name == "aloha":
openpi_obs, sglang_obs = _make_aloha_observation(rng, profile.prompt)
else:
raise ValueError(f"Unsupported Pi0.5 benchmark profile: {profile.name}")
openpi_observations.append(openpi_obs)
sglang_observations.append(sglang_obs)
return openpi_observations, sglang_observations
def _json_tensor(array: np.ndarray) -> dict[str, Any]:
return {
"dtype": str(array.dtype),
"shape": list(array.shape),
"values": array.tolist(),
}
def build_sglang_payload(
profile: Pi05BenchProfile,
observation: dict[str, Any],
*,
num_inference_steps: int,
prefix_cache: bool,
cuda_graph: bool,
noise: np.ndarray | None,
response_format: str = "envelope",
) -> dict[str, Any]:
encoded_images = {
key: _json_tensor(np.asarray(value))
for key, value in observation["images"].items()
}
encoded_observation = {
"images": encoded_images,
"state": _json_tensor(np.asarray(observation["state"], dtype=np.float32)),
}
if noise is not None:
encoded_observation["noise"] = _json_tensor(noise.astype(np.float32))
return {
"model": profile.sglang_model,
"input": {
"task": profile.prompt,
"observation": encoded_observation,
},
"parameters": {
"num_inference_steps": num_inference_steps,
},
"runtime": {
"return_timing": True,
"prefix_cache": prefix_cache,
"cuda_graph": cuda_graph,
"response_format": response_format,
},
}
def build_sglang_python_payload(
profile: Pi05BenchProfile,
observation: dict[str, Any],
*,
num_inference_steps: int,
prefix_cache: bool,
cuda_graph: bool,
noise: np.ndarray | None,
response_format: str = "envelope",
) -> dict[str, Any]:
encoded_observation = {
"images": {
key: np.asarray(value) for key, value in observation["images"].items()
},
"state": np.asarray(observation["state"], dtype=np.float32),
}
if noise is not None:
encoded_observation["noise"] = noise.astype(np.float32)
return {
"model": profile.sglang_model,
"input": {
"task": profile.prompt,
"observation": encoded_observation,
},
"parameters": {
"num_inference_steps": num_inference_steps,
},
"runtime": {
"return_timing": True,
"prefix_cache": prefix_cache,
"cuda_graph": cuda_graph,
"output_format": "numpy",
"response_format": response_format,
},
}
def build_sglang_openpi_ws_payload(
profile: Pi05BenchProfile,
observation: dict[str, Any],
*,
num_inference_steps: int,
prefix_cache: bool,
cuda_graph: bool,
noise: np.ndarray | None,
) -> dict[str, Any]:
payload: dict[str, Any] = {
"task": profile.prompt,
"observation.state": np.asarray(observation["state"], dtype=np.float32),
"num_inference_steps": num_inference_steps,
"enable_pi_prefix_cache": prefix_cache,
"enable_pi_cuda_graph": cuda_graph,
"output_format": "numpy",
}
for key, value in observation["images"].items():
payload[f"observation.images.{key}"] = np.asarray(value)
if noise is not None:
payload["observation.noise"] = noise.astype(np.float32)
return payload
def _post_action(
session: requests.Session,
url: str,
payload: dict[str, Any],
timeout_s: float,
) -> dict[str, Any]:
response = session.post(url, json=payload, timeout=timeout_s)
response.raise_for_status()
return response.json()
def _post_action_msgpack(
session: requests.Session,
url: str,
payload: dict[str, Any],
timeout_s: float,
) -> dict[str, Any]:
response = session.post(
url,
data=pack_msgpack(payload),
headers={
"Content-Type": "application/msgpack",
"Accept": "application/msgpack",
},
timeout=timeout_s,
)
response.raise_for_status()
return unpack_msgpack(response.content)
def _get_action_metadata(session: requests.Session, url: str, timeout_s: float):
response = session.get(
url.rstrip("/") + "/v1/actions/metadata",
timeout=timeout_s,
)
response.raise_for_status()
return response.json()
def run_sglang_http(
url: str,
payloads: list[dict[str, Any]],
*,
warmup: int,
repeats: int,
batch_size: int,
timeout_s: float,
msgpack: bool = False,
) -> dict[str, Any]:
endpoint = url.rstrip("/") + "/v1/actions/generations"
post_action = _post_action_msgpack if msgpack else _post_action
single_latencies = []
single_outputs = []
batch_latencies = []
with requests.Session() as session:
metadata = _get_action_metadata(session, url, timeout_s)
for idx in range(min(warmup, len(payloads))):
post_action(session, endpoint, payloads[idx], timeout_s)
for idx in range(repeats):
payload = payloads[idx % len(payloads)]
start = time.perf_counter()
output = post_action(session, endpoint, payload, timeout_s)
single_latencies.append((time.perf_counter() - start) * 1000)
single_outputs.append(output)
if batch_size > 1:
sessions = [requests.Session() for _ in range(batch_size)]
try:
with ThreadPoolExecutor(max_workers=batch_size) as pool:
def post_item(item):
session, payload = item
return post_action(session, endpoint, payload, timeout_s)
for warmup_idx in range(warmup):
batch = [
payloads[(warmup_idx * batch_size + offset) % len(payloads)]
for offset in range(batch_size)
]
list(pool.map(post_item, zip(sessions, batch)))
for start_idx in range(repeats):
batch = [
payloads[(start_idx * batch_size + offset) % len(payloads)]
for offset in range(batch_size)
]
start = time.perf_counter()
list(pool.map(post_item, zip(sessions, batch)))
batch_latencies.append((time.perf_counter() - start) * 1000)
finally:
for session in sessions:
session.close()
stage_timings = {}
for output in single_outputs:
for key, value in output.get("timings", {}).items():
stage_timings.setdefault(key, []).append(float(value))
return {
"single": _stats_ms(single_latencies),
"batch": _stats_ms(batch_latencies),
"batch_size": batch_size,
"stage_timings": {
key: _stats_ms(values) for key, values in stage_timings.items()
},
"first_output": single_outputs[0] if single_outputs else None,
"batch_mode": "concurrent_http_msgpack" if msgpack else "concurrent_http_json",
"metadata": metadata,
}
def _action_ws_url(url: str) -> str:
if url.startswith("https://"):
return "wss://" + url[len("https://") :].rstrip("/") + "/openpi/policy"
if url.startswith("http://"):
return "ws://" + url[len("http://") :].rstrip("/") + "/openpi/policy"
return url.rstrip("/") + "/openpi/policy"
async def _ws_send_recv(websocket, payload: dict[str, Any]) -> dict[str, Any]:
await websocket.send(pack_msgpack(payload))
response = await websocket.recv()
if isinstance(response, str):
raise RuntimeError(response)
return unpack_msgpack(response)
async def _run_sglang_openpi_ws_async(
url: str,
payloads: list[dict[str, Any]],
*,
warmup: int,
repeats: int,
batch_size: int,
) -> dict[str, Any]:
import websockets
endpoint = _action_ws_url(url)
async with websockets.connect(endpoint, max_size=None) as websocket:
metadata = unpack_msgpack(await websocket.recv())
for idx in range(min(warmup, len(payloads))):
await _ws_send_recv(websocket, payloads[idx])
single_latencies = []
single_outputs = []
for idx in range(repeats):
payload = payloads[idx % len(payloads)]
start = time.perf_counter()
output = await _ws_send_recv(websocket, payload)
single_latencies.append((time.perf_counter() - start) * 1000)
single_outputs.append(output)
batch_latencies = []
if batch_size > 1:
websockets_list = []
try:
for _ in range(batch_size):
websocket = await websockets.connect(endpoint, max_size=None)
await websocket.recv()
websockets_list.append(websocket)
for warmup_idx in range(warmup):
batch = [
payloads[(warmup_idx * batch_size + offset) % len(payloads)]
for offset in range(batch_size)
]
await asyncio.gather(
*[
_ws_send_recv(websocket, payload)
for websocket, payload in zip(websockets_list, batch)
]
)
for start_idx in range(repeats):
batch = [
payloads[(start_idx * batch_size + offset) % len(payloads)]
for offset in range(batch_size)
]
start = time.perf_counter()
await asyncio.gather(
*[
_ws_send_recv(websocket, payload)
for websocket, payload in zip(websockets_list, batch)
]
)
batch_latencies.append((time.perf_counter() - start) * 1000)
finally:
for websocket in websockets_list:
await websocket.close()
stage_timings = {}
server_timings = {}
for output in single_outputs:
for key, value in output.get("timings", {}).items():
stage_timings.setdefault(key, []).append(float(value))
for key, value in output.get("server_timing", {}).items():
server_timings.setdefault(key, []).append(float(value))
return {
"single": _stats_ms(single_latencies),
"batch": _stats_ms(batch_latencies),
"batch_size": batch_size,
"stage_timings": {
key: _stats_ms(values) for key, values in stage_timings.items()
},
"server_timings": {
key: _stats_ms(values) for key, values in server_timings.items()
},
"first_output": single_outputs[0] if single_outputs else None,
"batch_mode": "persistent_openpi_websocket",
"metadata": metadata,
}
def run_sglang_openpi_ws(
url: str,
payloads: list[dict[str, Any]],
*,
warmup: int,
repeats: int,
batch_size: int,
) -> dict[str, Any]:
return asyncio.run(
_run_sglang_openpi_ws_async(
url,
payloads,
warmup=warmup,
repeats=repeats,
batch_size=batch_size,
)
)
def create_sglang_python_pipeline(
model_path: str,
*,
pipeline_config_path: str | None,
):
from sglang.multimodal_gen.runtime.pipelines.pi05 import Pi05Pipeline
from sglang.multimodal_gen.runtime.pipelines_core.executors.sync_executor import (
SyncExecutor,
)
from sglang.multimodal_gen.runtime.server_args import (
ServerArgs,
set_global_server_args,
)
kwargs: dict[str, Any] = {
"model_path": model_path,
"warmup_mode": "off",
"num_gpus": 1,
}
if pipeline_config_path:
kwargs["pipeline_config_path"] = pipeline_config_path
server_args = ServerArgs.from_kwargs(**kwargs)
set_global_server_args(server_args)
pipeline = Pi05Pipeline(
model_path,
server_args,
executor=SyncExecutor(server_args=server_args),
)
return pipeline, server_args
def _make_sglang_python_req(server_args, payload: dict[str, Any]):
from sglang.multimodal_gen.runtime.entrypoints.utils import prepare_request
from sglang.multimodal_gen.runtime.entrypoints.vla.protocol import (
build_action_sampling_params,
)
sampling_params = build_action_sampling_params(payload, server_args)
req = prepare_request(server_args, sampling_params)
req.suppress_logs = True
return req
def _run_sglang_python_once(pipeline, server_args, payload: dict[str, Any]):
req = _make_sglang_python_req(server_args, payload)
output_batch = pipeline.forward(req, server_args)
if output_batch.error:
raise RuntimeError(output_batch.error)
if not output_batch.output:
raise RuntimeError("SGLang Python policy returned no output")
return output_batch.output[0]
def _run_sglang_python_group(pipeline, server_args, payloads: list[dict[str, Any]]):
reqs = [_make_sglang_python_req(server_args, payload) for payload in payloads]
output_batches = pipeline.forward_batch(reqs, server_args)
outputs = []
for output_batch in output_batches:
if output_batch.error:
raise RuntimeError(output_batch.error)
if not output_batch.output:
raise RuntimeError("SGLang Python grouped policy returned no output")
outputs.append(output_batch.output[0])
return outputs
def run_sglang_python(
model_path: str,
payloads: list[dict[str, Any]],
*,
pipeline_config_path: str | None,
warmup: int,
repeats: int,
batch_size: int,
batch_mode: str,
) -> dict[str, Any]:
pipeline, server_args = create_sglang_python_pipeline(
model_path,
pipeline_config_path=pipeline_config_path,
)
from sglang.multimodal_gen.runtime.entrypoints.vla.protocol import action_metadata
metadata = action_metadata(server_args)
metadata["precision"] = sglang_precision_metadata(pipeline)
for idx in range(min(warmup, len(payloads))):
_run_sglang_python_once(pipeline, server_args, payloads[idx])
single_latencies = []
single_outputs = []
for idx in range(repeats):
payload = payloads[idx % len(payloads)]
start = time.perf_counter()
output = _run_sglang_python_once(pipeline, server_args, payload)
single_latencies.append((time.perf_counter() - start) * 1000)
single_outputs.append(output)
batch_latencies = []
batch_outputs = []
if batch_size > 1:
for warmup_idx in range(warmup):
batch = [
payloads[(warmup_idx * batch_size + offset) % len(payloads)]
for offset in range(batch_size)
]
if batch_mode == "grouped":
_run_sglang_python_group(pipeline, server_args, batch)
else:
for payload in batch:
_run_sglang_python_once(pipeline, server_args, payload)
for start_idx in range(repeats):
batch = [
payloads[(start_idx * batch_size + offset) % len(payloads)]
for offset in range(batch_size)
]
start = time.perf_counter()
if batch_mode == "grouped":
outputs = _run_sglang_python_group(pipeline, server_args, batch)
else:
outputs = []
for payload in batch:
outputs.append(
_run_sglang_python_once(pipeline, server_args, payload)
)
batch_latencies.append((time.perf_counter() - start) * 1000)
batch_outputs.extend(outputs)
stage_timings = {}
for output in single_outputs:
for key, value in output.get("timings", {}).items():
stage_timings.setdefault(key, []).append(float(value))
batch_stage_timings = {}
for output in batch_outputs:
for key, value in output.get("timings", {}).items():
batch_stage_timings.setdefault(key, []).append(float(value))
return {
"single": _stats_ms(single_latencies),
"batch": _stats_ms(batch_latencies),
"batch_size": batch_size,
"stage_timings": {
key: _stats_ms(values) for key, values in stage_timings.items()
},
"batch_stage_timings": {
key: _stats_ms(values) for key, values in batch_stage_timings.items()
},
"first_output": single_outputs[0] if single_outputs else None,
"batch_mode": f"python_policy_{batch_mode}",
"metadata": metadata,
}
def create_openpi_policy(
config_name: str,
checkpoint_dir: str,
*,
pytorch_device: str,
num_inference_steps: int,
pytorch_compile_mode: str | None,
):
from openpi.policies import policy_config
from openpi.training import config as openpi_config
train_config = openpi_config.get_config(config_name)
if pytorch_compile_mode != "keep":
train_config = dataclasses.replace(
train_config,
model=dataclasses.replace(
train_config.model,
pytorch_compile_mode=pytorch_compile_mode,
),
)
return policy_config.create_trained_policy(
train_config,
checkpoint_dir,
sample_kwargs={"num_steps": num_inference_steps},
pytorch_device=pytorch_device,
)
def _openpi_infer(policy, observation: dict[str, Any], noise: np.ndarray | None):
if noise is None:
return policy.infer(observation)
return policy.infer(observation, noise=noise)
def _openpi_direct_batch(
policy,
observations: list[dict[str, Any]],
noises: list[np.ndarray] | None,
):
import jax
import numpy as onp
from openpi.models import model as openpi_model
inputs_list = [
policy._input_transform(jax.tree.map(lambda value: value, observation))
for observation in observations
]
batched_inputs = jax.tree.map(
lambda *values: onp.stack(values, axis=0),
*inputs_list,
)
sample_kwargs = dict(policy._sample_kwargs)
if noises is not None:
noise = onp.stack(noises, axis=0)
if policy._is_pytorch_model:
import torch
sample_kwargs["noise"] = torch.from_numpy(noise).to(policy._pytorch_device)
else:
import jax.numpy as jnp
sample_kwargs["noise"] = jnp.asarray(noise)
if policy._is_pytorch_model:
import torch
inputs = jax.tree.map(
lambda value: torch.from_numpy(onp.asarray(value)).to(
policy._pytorch_device
),
batched_inputs,
)
sample_key_or_device = policy._pytorch_device
else:
import jax.numpy as jnp
inputs = jax.tree.map(lambda value: jnp.asarray(value), batched_inputs)
policy._rng, sample_key_or_device = jax.random.split(policy._rng)
observation = openpi_model.Observation.from_dict(inputs)
actions = policy._sample_actions(sample_key_or_device, observation, **sample_kwargs)
if policy._is_pytorch_model:
actions_np = actions.detach().cpu().numpy()
states_np = inputs["state"].detach().cpu().numpy()
else:
actions_np = onp.asarray(actions)
states_np = onp.asarray(inputs["state"])
outputs = []
for idx in range(actions_np.shape[0]):
outputs.append(
policy._output_transform(
{
"state": states_np[idx],
"actions": actions_np[idx],
}
)
)
return outputs
def run_openpi_policy(
policy,
observations: list[dict[str, Any]],
*,
warmup: int,
repeats: int,
batch_size: int,
noise: np.ndarray | None,
batch_mode: str,
) -> dict[str, Any]:
for idx in range(min(warmup, len(observations))):
_openpi_infer(policy, observations[idx], noise)
single_latencies = []
single_outputs = []
for idx in range(repeats):
observation = observations[idx % len(observations)]
start = time.perf_counter()
output = _openpi_infer(policy, observation, noise)
single_latencies.append((time.perf_counter() - start) * 1000)
single_outputs.append(output)
batch_latencies = []
if batch_size > 1:
for warmup_idx in range(warmup):
batch = [
observations[(warmup_idx * batch_size + offset) % len(observations)]
for offset in range(batch_size)
]
noises = [noise] * len(batch) if noise is not None else None
if batch_mode == "direct_model":
_openpi_direct_batch(policy, batch, noises)
elif batch_mode == "policy_loop":
for obs in batch:
_openpi_infer(policy, obs, noise)
else:
raise ValueError(f"Unsupported OpenPI batch mode: {batch_mode}")
for start_idx in range(repeats):
batch = [
observations[(start_idx * batch_size + offset) % len(observations)]
for offset in range(batch_size)
]
noises = [noise] * len(batch) if noise is not None else None
start = time.perf_counter()
if batch_mode == "direct_model":
_openpi_direct_batch(policy, batch, noises)
elif batch_mode == "policy_loop":
for obs in batch:
_openpi_infer(policy, obs, noise)
else:
raise ValueError(f"Unsupported OpenPI batch mode: {batch_mode}")
batch_latencies.append((time.perf_counter() - start) * 1000)
policy_timings = {}
for output in single_outputs:
for key, value in output.get("policy_timing", {}).items():
policy_timings.setdefault(key, []).append(float(value))
precision = openpi_precision_metadata(policy)
first_actions = _openpi_actions(single_outputs[0]) if single_outputs else None
if first_actions is not None:
precision["output_action_dtype"] = str(first_actions.dtype)
precision["output_action_shape"] = list(first_actions.shape)
return {
"single": _stats_ms(single_latencies),
"batch": _stats_ms(batch_latencies),
"batch_size": batch_size,
"policy_timings": {
key: _stats_ms(values) for key, values in policy_timings.items()
},
"first_output": single_outputs[0] if single_outputs else None,
"batch_mode": batch_mode,
"precision": precision,
}
def _sglang_actions(output: dict[str, Any]) -> np.ndarray | None:
if output is None:
return None
if "actions" in output:
return np.asarray(output["actions"], dtype=np.float32)
return np.asarray(output["data"][0]["action"]["values"], dtype=np.float32)
def _openpi_actions(output: dict[str, Any]) -> np.ndarray | None:
if output is None:
return None
return np.asarray(output["actions"], dtype=np.float32)
def compare_first_actions(
sglang_output: dict[str, Any] | None,
openpi_output: dict[str, Any] | None,
) -> dict[str, Any]:
sglang_actions = _sglang_actions(sglang_output)
openpi_actions = _openpi_actions(openpi_output)
if sglang_actions is None or openpi_actions is None:
return {"available": False}
horizon = min(sglang_actions.shape[0], openpi_actions.shape[0])
dim = min(sglang_actions.shape[1], openpi_actions.shape[1])
if horizon == 0 or dim == 0:
return {
"available": False,
"sglang_shape": list(sglang_actions.shape),
"openpi_shape": list(openpi_actions.shape),
}
diff = np.abs(sglang_actions[:horizon, :dim] - openpi_actions[:horizon, :dim])
return {
"available": True,
"common_shape": [int(horizon), int(dim)],
"sglang_shape": list(sglang_actions.shape),
"openpi_shape": list(openpi_actions.shape),
"max_abs_diff": float(np.max(diff)),
"mean_abs_diff": float(np.mean(diff)),
}
def print_summary(result: dict[str, Any]) -> None:
print(json.dumps(result, indent=2, sort_keys=True))
sglang_result = result.get("sglang") or {}
openpi_result = result.get("openpi") or {}
sgl = sglang_result.get("single", {}).get("mean_ms")
opi = openpi_result.get("single", {}).get("mean_ms")
if sgl and opi:
print(
"\nSingle mean latency: "
f"SGLang={sgl:.2f} ms, OpenPI={opi:.2f} ms, "
f"speedup={opi / sgl:.2f}x"
)
sgl_batch = sglang_result.get("batch", {}).get("mean_ms")
opi_batch = openpi_result.get("batch", {}).get("mean_ms")
batch_size = sglang_result.get("batch_size") or openpi_result.get("batch_size", 0)
if sgl_batch and opi_batch and batch_size:
print(
"Batch mean latency: "
f"SGLang={sgl_batch:.2f} ms/{batch_size} req, "
f"OpenPI={opi_batch:.2f} ms/{batch_size} req, "
f"speedup={opi_batch / sgl_batch:.2f}x"
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--profile", choices=sorted(PROFILES), default="libero")
parser.add_argument("--sglang-url", default="http://127.0.0.1:30000")
parser.add_argument(
"--sglang-api",
choices=("http", "http_msgpack", "openpi_ws", "python"),
default="http",
)
parser.add_argument("--sglang-model", default=None)
parser.add_argument("--sglang-pipeline-config-path", default=None)
parser.add_argument(
"--sglang-http-response-format",
choices=("envelope", "raw"),
default="envelope",
)
parser.add_argument(
"--sglang-python-batch-mode",
choices=("loop", "grouped"),
default="loop",
)
parser.add_argument("--openpi-config", default=None)
parser.add_argument("--openpi-checkpoint", default=None)
parser.add_argument("--openpi-device", default="cuda")
parser.add_argument(
"--openpi-pytorch-compile-mode",
choices=(
"keep",
"none",
"default",
"reduce-overhead",
"max-autotune",
"max-autotune-no-cudagraphs",
),
default="keep",
)
parser.add_argument(
"--openpi-batch-mode",
choices=("direct_model", "policy_loop"),
default="direct_model",
)
parser.add_argument("--num-inference-steps", type=int, default=10)
parser.add_argument("--num-samples", type=int, default=16)
parser.add_argument("--repeats", type=int, default=20)
parser.add_argument("--warmup", type=int, default=3)
parser.add_argument("--batch-size", type=int, default=4)
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--timeout-s", type=float, default=180.0)
parser.add_argument("--disable-prefix-cache", action="store_true")
parser.add_argument("--disable-cuda-graph", action="store_true")
parser.add_argument("--deterministic-noise", action="store_true")
parser.add_argument("--skip-sglang", action="store_true")
parser.add_argument("--skip-openpi", action="store_true")
parser.add_argument("--output-file", default="")
return parser.parse_args()
def main() -> None:
args = parse_args()
profile = PROFILES[args.profile]
if args.sglang_model:
profile = Pi05BenchProfile(
name=profile.name,
sglang_model=args.sglang_model,
openpi_config=profile.openpi_config,
openpi_checkpoint=profile.openpi_checkpoint,
prompt=profile.prompt,
sglang_action_horizon=profile.sglang_action_horizon,
openpi_action_horizon=profile.openpi_action_horizon,
action_dim=profile.action_dim,
output_action_dim=profile.output_action_dim,
)
openpi_config = args.openpi_config or profile.openpi_config
openpi_checkpoint = args.openpi_checkpoint or profile.openpi_checkpoint
openpi_observations, sglang_observations = build_observations(
profile,
max(args.num_samples, args.batch_size),
args.seed,
)
noise = None
if args.skip_sglang and args.skip_openpi:
raise ValueError("At least one backend must be enabled")
if args.deterministic_noise:
if profile.sglang_action_horizon != profile.openpi_action_horizon:
raise ValueError(
"--deterministic-noise requires matching SGLang and OpenPI "
f"action horizons; profile {profile.name!r} has "
f"{profile.sglang_action_horizon} vs {profile.openpi_action_horizon}"
)
rng = np.random.default_rng(args.seed + 1)
noise = rng.standard_normal(
(profile.sglang_action_horizon, profile.action_dim),
dtype=np.float32,
)
payloads = []
if args.skip_sglang:
pass
elif args.sglang_api in ("python", "http_msgpack"):
payloads = [
build_sglang_python_payload(
profile,
observation,
num_inference_steps=args.num_inference_steps,
prefix_cache=not args.disable_prefix_cache,
cuda_graph=not args.disable_cuda_graph,
noise=noise,
response_format=args.sglang_http_response_format,
)
for observation in sglang_observations
]
elif args.sglang_api == "openpi_ws":
payloads = [
build_sglang_openpi_ws_payload(
profile,
observation,
num_inference_steps=args.num_inference_steps,
prefix_cache=not args.disable_prefix_cache,
cuda_graph=not args.disable_cuda_graph,
noise=noise,
)
for observation in sglang_observations
]
else:
payloads = [
build_sglang_payload(
profile,
observation,
num_inference_steps=args.num_inference_steps,
prefix_cache=not args.disable_prefix_cache,
cuda_graph=not args.disable_cuda_graph,
noise=noise,
response_format=args.sglang_http_response_format,
)
for observation in sglang_observations
]
openpi_policy = None
if not args.skip_openpi:
openpi_policy = create_openpi_policy(
openpi_config,
openpi_checkpoint,
pytorch_device=args.openpi_device,
num_inference_steps=args.num_inference_steps,
pytorch_compile_mode=(
None
if args.openpi_pytorch_compile_mode == "none"
else args.openpi_pytorch_compile_mode
),
)
sglang_result = None
if args.skip_sglang:
pass
elif args.sglang_api == "python":
sglang_result = run_sglang_python(
profile.sglang_model,
payloads,
pipeline_config_path=args.sglang_pipeline_config_path,
warmup=args.warmup,
repeats=args.repeats,
batch_size=args.batch_size,
batch_mode=args.sglang_python_batch_mode,
)
elif args.sglang_api == "openpi_ws":
sglang_result = run_sglang_openpi_ws(
args.sglang_url,
payloads,
warmup=args.warmup,
repeats=args.repeats,
batch_size=args.batch_size,
)
else:
sglang_result = run_sglang_http(
args.sglang_url,
payloads,
warmup=args.warmup,
repeats=args.repeats,
batch_size=args.batch_size,
timeout_s=args.timeout_s,
msgpack=args.sglang_api == "http_msgpack",
)
openpi_result = None
if openpi_policy is not None:
openpi_result = run_openpi_policy(
openpi_policy,
openpi_observations,
warmup=args.warmup,
repeats=args.repeats,
batch_size=args.batch_size,
noise=noise,
batch_mode=args.openpi_batch_mode,
)
result = {
"profile": profile.name,
"sglang_model": profile.sglang_model,
"sglang_api": args.sglang_api,
"sglang_pipeline_config_path": args.sglang_pipeline_config_path,
"sglang_http_response_format": args.sglang_http_response_format,
"sglang_python_batch_mode": args.sglang_python_batch_mode,
"openpi_config": openpi_config,
"openpi_checkpoint": openpi_checkpoint,
"openpi_pytorch_compile_mode": args.openpi_pytorch_compile_mode,
"num_inference_steps": args.num_inference_steps,
"num_samples": args.num_samples,
"repeats": args.repeats,
"warmup": args.warmup,
"deterministic_noise": args.deterministic_noise,
"action_diff": compare_first_actions(
None if sglang_result is None else sglang_result.get("first_output"),
None if openpi_result is None else openpi_result.get("first_output"),
),
"sglang": (
None
if sglang_result is None
else {
key: value
for key, value in sglang_result.items()
if key not in ("first_output",)
}
),
"openpi": (
None
if openpi_result is None
else {
key: value
for key, value in openpi_result.items()
if key not in ("first_output",)
}
),
}
if args.output_file:
with open(args.output_file, "w", encoding="utf-8") as f:
json.dump(result, f, indent=2, sort_keys=True)
print_summary(result)
if __name__ == "__main__":
main()