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
297 lines
9.7 KiB
Python
297 lines
9.7 KiB
Python
import asyncio
|
|
import os
|
|
import time
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from fastapi import (
|
|
APIRouter,
|
|
File,
|
|
Form,
|
|
HTTPException,
|
|
Path,
|
|
Query,
|
|
Request,
|
|
UploadFile,
|
|
)
|
|
from fastapi.responses import FileResponse
|
|
|
|
from sglang.multimodal_gen.configs.sample.sampling_params import (
|
|
SamplingParams,
|
|
generate_request_id,
|
|
)
|
|
from sglang.multimodal_gen.runtime.entrypoints.openai.protocol import (
|
|
MeshGenerationsRequest,
|
|
MeshListResponse,
|
|
MeshResponse,
|
|
)
|
|
from sglang.multimodal_gen.runtime.entrypoints.openai.storage import cloud_storage
|
|
from sglang.multimodal_gen.runtime.entrypoints.openai.stores import MESH_STORE
|
|
from sglang.multimodal_gen.runtime.entrypoints.openai.utils import (
|
|
add_common_data_to_response,
|
|
merge_image_input_list,
|
|
process_generation_batch,
|
|
save_image_to_path,
|
|
)
|
|
from sglang.multimodal_gen.runtime.entrypoints.utils import prepare_request
|
|
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
|
|
from sglang.multimodal_gen.runtime.server_args import get_global_server_args
|
|
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
|
|
|
logger = init_logger(__name__)
|
|
router = APIRouter(prefix="/v1/meshes", tags=["meshes"])
|
|
|
|
|
|
def _normalize_format(fmt: Optional[str]) -> str:
|
|
fmt = (fmt or "glb").lower()
|
|
return fmt if fmt in ("glb", "obj") else "glb"
|
|
|
|
|
|
def _build_sampling_params_from_request(
|
|
request_id: str, req: MeshGenerationsRequest, image_path: Optional[str] = None
|
|
) -> SamplingParams:
|
|
ext = _normalize_format(req.output_format)
|
|
|
|
server_args = get_global_server_args()
|
|
sampling_kwargs: Dict[str, Any] = {
|
|
"request_id": request_id,
|
|
"prompt": req.prompt,
|
|
"num_frames": 1,
|
|
"image_path": [image_path] if image_path else None,
|
|
"save_output": True,
|
|
"output_file_name": f"{request_id}.{ext}",
|
|
"seed": req.seed,
|
|
"generator_device": req.generator_device,
|
|
}
|
|
if req.num_inference_steps is not None:
|
|
sampling_kwargs["num_inference_steps"] = req.num_inference_steps
|
|
if req.guidance_scale is not None:
|
|
sampling_kwargs["guidance_scale"] = req.guidance_scale
|
|
if req.negative_prompt is not None:
|
|
sampling_kwargs["negative_prompt"] = req.negative_prompt
|
|
|
|
return SamplingParams.from_user_sampling_params_args(
|
|
model_path=server_args.model_path,
|
|
server_args=server_args,
|
|
**sampling_kwargs,
|
|
)
|
|
|
|
|
|
def _mesh_job_from_sampling(
|
|
request_id: str, req: MeshGenerationsRequest, sampling: SamplingParams
|
|
) -> Dict[str, Any]:
|
|
return {
|
|
"id": request_id,
|
|
"object": "mesh",
|
|
"model": req.model or "",
|
|
"status": "queued",
|
|
"progress": 0,
|
|
"created_at": int(time.time()),
|
|
"format": _normalize_format(req.output_format),
|
|
"file_path": os.path.abspath(sampling.output_file_path()),
|
|
}
|
|
|
|
|
|
async def _dispatch_job_async(job_id: str, batch: Req) -> None:
|
|
from sglang.multimodal_gen.runtime.scheduler_client import async_scheduler_client
|
|
|
|
try:
|
|
save_file_path_list, result = await process_generation_batch(
|
|
async_scheduler_client, batch
|
|
)
|
|
save_file_path = save_file_path_list[0]
|
|
|
|
file_size = None
|
|
if os.path.exists(save_file_path):
|
|
file_size = os.path.getsize(save_file_path)
|
|
|
|
cloud_url = await cloud_storage.upload_and_cleanup(save_file_path)
|
|
|
|
update_fields: Dict[str, Any] = {
|
|
"status": "completed",
|
|
"progress": 100,
|
|
"completed_at": int(time.time()),
|
|
"url": cloud_url,
|
|
"file_path": save_file_path if not cloud_url else None,
|
|
"file_size_bytes": file_size,
|
|
}
|
|
update_fields = add_common_data_to_response(
|
|
update_fields, request_id=job_id, result=result
|
|
)
|
|
await MESH_STORE.update_fields(job_id, update_fields)
|
|
except Exception as e:
|
|
logger.error(f"{e}")
|
|
await MESH_STORE.update_fields(
|
|
job_id, {"status": "failed", "error": {"message": str(e)}}
|
|
)
|
|
|
|
|
|
@router.post("", response_model=MeshResponse)
|
|
async def create_mesh(
|
|
request: Request,
|
|
image: Optional[List[UploadFile]] = File(None),
|
|
image_array: Optional[List[UploadFile]] = File(None, alias="image[]"),
|
|
url: Optional[List[str]] = Form(None),
|
|
url_array: Optional[List[str]] = Form(None, alias="url[]"),
|
|
prompt: Optional[str] = Form("generate 3d mesh"),
|
|
model: Optional[str] = Form(None),
|
|
seed: Optional[int] = Form(None),
|
|
generator_device: Optional[str] = Form("cuda"),
|
|
guidance_scale: Optional[float] = Form(None),
|
|
num_inference_steps: Optional[int] = Form(None),
|
|
negative_prompt: Optional[str] = Form(None),
|
|
output_format: Optional[str] = Form("glb"),
|
|
):
|
|
content_type = request.headers.get("content-type", "").lower()
|
|
request_id = generate_request_id()
|
|
server_args = get_global_server_args()
|
|
|
|
input_path = None
|
|
|
|
if "multipart/form-data" in content_type:
|
|
images = image or image_array
|
|
urls = url or url_array
|
|
image_list = merge_image_input_list(images, urls)
|
|
|
|
if not image_list:
|
|
raise HTTPException(
|
|
status_code=422,
|
|
detail="Field 'image' or 'url' is required for mesh generation",
|
|
)
|
|
|
|
uploads_dir = os.path.join("outputs", "uploads")
|
|
os.makedirs(uploads_dir, exist_ok=True)
|
|
img = image_list[0]
|
|
filename = img.filename if hasattr(img, "filename") else "input_image"
|
|
try:
|
|
input_path = await save_image_to_path(
|
|
img, os.path.join(uploads_dir, f"{request_id}_{filename}")
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=400, detail=f"Failed to process image source: {str(e)}"
|
|
)
|
|
|
|
req = MeshGenerationsRequest(
|
|
prompt=prompt or "generate 3d mesh",
|
|
model=model,
|
|
seed=seed,
|
|
generator_device=generator_device,
|
|
num_inference_steps=num_inference_steps,
|
|
negative_prompt=negative_prompt,
|
|
output_format=output_format,
|
|
**(
|
|
{"guidance_scale": guidance_scale} if guidance_scale is not None else {}
|
|
),
|
|
)
|
|
else:
|
|
try:
|
|
body = await request.json()
|
|
except Exception:
|
|
body = {}
|
|
try:
|
|
payload: Dict[str, Any] = dict(body or {})
|
|
|
|
if payload.get("input_image"):
|
|
img_src = payload.pop("input_image")
|
|
uploads_dir = os.path.join("outputs", "uploads")
|
|
os.makedirs(uploads_dir, exist_ok=True)
|
|
input_path = await save_image_to_path(
|
|
img_src,
|
|
os.path.join(uploads_dir, f"{request_id}_input_image"),
|
|
)
|
|
|
|
req = MeshGenerationsRequest(**payload)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=f"Invalid request body: {e}")
|
|
|
|
if not input_path:
|
|
raise HTTPException(
|
|
status_code=422,
|
|
detail="An input image is required for mesh generation",
|
|
)
|
|
|
|
sampling_params = _build_sampling_params_from_request(request_id, req, input_path)
|
|
job = _mesh_job_from_sampling(request_id, req, sampling_params)
|
|
await MESH_STORE.upsert(request_id, job)
|
|
|
|
batch = prepare_request(
|
|
server_args=server_args,
|
|
sampling_params=sampling_params,
|
|
)
|
|
|
|
asyncio.create_task(_dispatch_job_async(request_id, batch))
|
|
return MeshResponse(**job)
|
|
|
|
|
|
@router.get("", response_model=MeshListResponse)
|
|
async def list_meshes(
|
|
after: Optional[str] = Query(None),
|
|
limit: Optional[int] = Query(None, ge=1, le=100),
|
|
order: Optional[str] = Query("desc"),
|
|
):
|
|
order = (order or "desc").lower()
|
|
if order not in ("asc", "desc"):
|
|
order = "desc"
|
|
jobs = await MESH_STORE.list_values()
|
|
|
|
reverse = order != "asc"
|
|
jobs.sort(key=lambda j: j.get("created_at", 0), reverse=reverse)
|
|
|
|
if after is not None:
|
|
try:
|
|
idx = next(i for i, j in enumerate(jobs) if j["id"] == after)
|
|
jobs = jobs[idx + 1 :]
|
|
except StopIteration:
|
|
jobs = []
|
|
|
|
if limit is not None:
|
|
jobs = jobs[:limit]
|
|
items = [MeshResponse(**j) for j in jobs]
|
|
return MeshListResponse(data=items)
|
|
|
|
|
|
@router.get("/{mesh_id}", response_model=MeshResponse)
|
|
async def retrieve_mesh(mesh_id: str = Path(...)):
|
|
job = await MESH_STORE.get(mesh_id)
|
|
if not job:
|
|
raise HTTPException(status_code=404, detail="Mesh not found")
|
|
return MeshResponse(**job)
|
|
|
|
|
|
@router.delete("/{mesh_id}", response_model=MeshResponse)
|
|
async def delete_mesh(mesh_id: str = Path(...)):
|
|
job = await MESH_STORE.pop(mesh_id)
|
|
if not job:
|
|
raise HTTPException(status_code=404, detail="Mesh not found")
|
|
job["status"] = "deleted"
|
|
return MeshResponse(**job)
|
|
|
|
|
|
@router.get("/{mesh_id}/content")
|
|
async def download_mesh_content(
|
|
mesh_id: str = Path(...), variant: Optional[str] = Query(None)
|
|
):
|
|
job = await MESH_STORE.get(mesh_id)
|
|
if not job:
|
|
raise HTTPException(status_code=404, detail="Mesh not found")
|
|
|
|
if job.get("url"):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Mesh has been uploaded to cloud storage. Please use the cloud URL: {job.get('url')}",
|
|
)
|
|
|
|
file_path = job.get("file_path")
|
|
if not file_path or not os.path.exists(file_path):
|
|
raise HTTPException(status_code=404, detail="Generation is still in-progress")
|
|
|
|
ext = os.path.splitext(file_path)[1].lower()
|
|
media_type = {
|
|
".glb": "model/gltf-binary",
|
|
".obj": "text/plain",
|
|
}.get(ext, "application/octet-stream")
|
|
|
|
return FileResponse(
|
|
path=file_path, media_type=media_type, filename=os.path.basename(file_path)
|
|
)
|