create rust workspace (#32014)
This commit is contained in:
@@ -23,6 +23,14 @@ jobs:
|
|||||||
python -m pip install pre-commit
|
python -m pip install pre-commit
|
||||||
pre-commit install
|
pre-commit install
|
||||||
|
|
||||||
|
# The clippy-rust-workspace pre-commit hook compiles the rust/ workspace
|
||||||
|
# (debug profile); cache cargo registry + target across runs.
|
||||||
|
- name: Rust cache (rust/ workspace)
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: rust
|
||||||
|
shared-key: "rust-workspace-lint"
|
||||||
|
|
||||||
- name: Run pre-commit checks
|
- name: Run pre-commit checks
|
||||||
run: SKIP=no-commit-to-branch pre-commit run --all-files --show-diff-on-failure
|
run: SKIP=no-commit-to-branch pre-commit run --all-files --show-diff-on-failure
|
||||||
|
|
||||||
|
|||||||
@@ -309,10 +309,10 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
run: bash scripts/ci/utils/install_rust_protoc.sh
|
run: bash scripts/ci/utils/install_rust_protoc.sh
|
||||||
|
|
||||||
- name: Rust cache (sglang-grpc)
|
- name: Rust cache (rust/ workspace)
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: rust/sglang-grpc
|
workspaces: rust
|
||||||
shared-key: "sglang-grpc-cpu"
|
shared-key: "sglang-grpc-cpu"
|
||||||
save-if: ${{ matrix.partition == 0 }}
|
save-if: ${{ matrix.partition == 0 }}
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,21 @@ repos:
|
|||||||
language: system
|
language: system
|
||||||
files: ^experimental/sgl-router/.*\.rs$
|
files: ^experimental/sgl-router/.*\.rs$
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
- id: clippy-rust-workspace
|
||||||
|
name: clippy rust/ workspace (auto-fix)
|
||||||
|
# Two steps: apply machine-applicable fixes, then fail on anything left
|
||||||
|
# (combining --fix with -D warnings can abort the fix phase). protoc is
|
||||||
|
# not required: sglang-grpc's build.rs falls back to a vendored binary.
|
||||||
|
entry: bash -c 'cd rust && cargo clippy --workspace --fix --allow-dirty --allow-staged && cargo clippy --workspace -- -D warnings'
|
||||||
|
language: system
|
||||||
|
files: ^rust/.*\.rs$
|
||||||
|
pass_filenames: false
|
||||||
|
- id: rustfmt-rust-workspace
|
||||||
|
name: rustfmt rust/ workspace
|
||||||
|
entry: bash -c 'cd rust && cargo fmt'
|
||||||
|
language: system
|
||||||
|
files: ^rust/.*\.rs$
|
||||||
|
pass_filenames: false
|
||||||
- id: mint-broken-links
|
- id: mint-broken-links
|
||||||
name: check docs_new links (Mintlify; opt-in, requires `mint` on PATH)
|
name: check docs_new links (Mintlify; opt-in, requires `mint` on PATH)
|
||||||
entry: bash -c 'cd docs_new && mint broken-links --check-anchors --check-redirects'
|
entry: bash -c 'cd docs_new && mint broken-links --check-anchors --check-redirects'
|
||||||
|
|||||||
+4
-10
@@ -225,16 +225,10 @@ git_describe_command = ["python3", "python/tools/get_version_tag.py"]
|
|||||||
# Allow editable installs even when .git metadata is not available.
|
# Allow editable installs even when .git metadata is not available.
|
||||||
fallback_version = "0.0.0.dev0"
|
fallback_version = "0.0.0.dev0"
|
||||||
|
|
||||||
[[tool.setuptools-rust.ext-modules]]
|
# Rust extension modules are auto-discovered by setup.py from the cargo
|
||||||
target = "sglang.srt.grpc._core"
|
# workspace in ../rust ([package.metadata.sglang] python-module in each crate).
|
||||||
path = "../rust/sglang-grpc/Cargo.toml"
|
# This CUDA pyproject builds all of them; platform variants restrict the set
|
||||||
binding = "PyO3"
|
# via [tool.sglang] rust-extensions (see pyproject_other.toml).
|
||||||
|
|
||||||
[[tool.setuptools-rust.ext-modules]]
|
|
||||||
target = "sglang.srt.multimodal._core"
|
|
||||||
path = "../rust/sglang-mm/Cargo.toml"
|
|
||||||
binding = "PyO3"
|
|
||||||
debug = false
|
|
||||||
|
|
||||||
[tool.kernels.dependencies]
|
[tool.kernels.dependencies]
|
||||||
"kernels-community/sgl-flash-attn3" = 1
|
"kernels-community/sgl-flash-attn3" = 1
|
||||||
|
|||||||
@@ -192,12 +192,14 @@ dev_mps = ["sglang[all_mps]", "sglang[test]"]
|
|||||||
[project.scripts]
|
[project.scripts]
|
||||||
sglang = "sglang.cli.main:main"
|
sglang = "sglang.cli.main:main"
|
||||||
|
|
||||||
# Rust-accelerated multimodal preprocessing (sglang.srt.multimodal._core).
|
# Rust extension modules are auto-discovered by setup.py from the cargo
|
||||||
# grpc is intentionally omitted here (it needs proto/tonic); ROCm only builds mm.
|
# workspace in ../rust; this allowlist restricts which of them this platform
|
||||||
[[tool.setuptools-rust.ext-modules]]
|
# builds (case-insensitive substrings of the target module). grpc is
|
||||||
target = "sglang.srt.multimodal._core"
|
# intentionally omitted here (it needs proto/tonic); ROCm only builds mm.
|
||||||
path = "../rust/sglang-mm/Cargo.toml"
|
# NOTE: setup.py reads this with a line regex, not a TOML parser — keep the
|
||||||
binding = "PyO3"
|
# assignment on a single line.
|
||||||
|
[tool.sglang]
|
||||||
|
rust-extensions = ["multimodal"]
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
"sglang" = [
|
"sglang" = [
|
||||||
|
|||||||
+144
-37
@@ -1,39 +1,152 @@
|
|||||||
"""sglang build hooks.
|
"""sglang build hooks.
|
||||||
|
|
||||||
SGLANG_BUILD_RUST_EXTS controls which Rust extensions are built:
|
Rust extensions are auto-discovered from the cargo workspace in ../rust: every
|
||||||
- unset or "all": build every declared Rust extension (the default).
|
crate whose Cargo.toml declares
|
||||||
- "none": build no Rust extensions.
|
|
||||||
- comma-separated names: build only extensions whose target matches one of the
|
|
||||||
given (case-insensitive) substrings, e.g. "grpc" matches
|
|
||||||
"sglang.srt.grpc._core".
|
|
||||||
|
|
||||||
This is a build-time environment variable, so it is read directly from
|
[package.metadata.sglang]
|
||||||
os.environ instead of sglang.srt.environ, which is not available until after the
|
python-module = "sglang.srt.<pkg>._core" # import path inside the wheel
|
||||||
package has been built.
|
debug = false # optional RustExtension knob
|
||||||
|
|
||||||
|
is built as a PyO3 extension module at that import path. Adding a new extension
|
||||||
|
crate therefore needs no pyproject changes — declare the metadata in the crate.
|
||||||
|
|
||||||
|
Two filters can narrow the discovered set:
|
||||||
|
|
||||||
|
- [tool.sglang] rust-extensions in the active pyproject.toml: a list of
|
||||||
|
case-insensitive substrings of the target module. Platform pyprojects use
|
||||||
|
this to build a subset (e.g. pyproject_other.toml builds only "multimodal";
|
||||||
|
grpc needs proto/tonic and is intentionally CUDA-only).
|
||||||
|
- SGLANG_BUILD_RUST_EXTS env var, applied at build time on top of the above:
|
||||||
|
unset or "all" builds everything, "none" builds nothing, and a
|
||||||
|
comma-separated list matches substrings, e.g. "grpc" matches
|
||||||
|
"sglang.srt.grpc._core". It is read directly from os.environ instead of
|
||||||
|
sglang.srt.environ, which is not importable until the package is built.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from setuptools_rust import build_rust
|
from setuptools_rust import Binding, RustExtension, build_rust
|
||||||
except ModuleNotFoundError as exc:
|
except ModuleNotFoundError as exc:
|
||||||
if exc.name != "setuptools_rust":
|
if exc.name != "setuptools_rust":
|
||||||
raise
|
raise
|
||||||
# Alternate platform pyprojects do not declare Rust extensions.
|
# Alternate platform pyprojects that build no Rust extensions do not
|
||||||
|
# install setuptools-rust.
|
||||||
build_rust = None
|
build_rust = None
|
||||||
|
|
||||||
_BUILD_RUST_EXTS_ENV = "SGLANG_BUILD_RUST_EXTS"
|
_BUILD_RUST_EXTS_ENV = "SGLANG_BUILD_RUST_EXTS"
|
||||||
|
_PYTHON_DIR = Path(__file__).resolve().parent
|
||||||
|
_RUST_WORKSPACE_DIR = _PYTHON_DIR.parent / "rust"
|
||||||
|
|
||||||
|
|
||||||
|
def _cargo_workspace_metadata():
|
||||||
|
"""The rust/ cargo workspace as JSON, straight from cargo's own parser."""
|
||||||
|
manifest_path = _RUST_WORKSPACE_DIR / "Cargo.toml"
|
||||||
|
if not manifest_path.is_file():
|
||||||
|
raise RuntimeError(
|
||||||
|
f"no cargo workspace at {manifest_path} (building outside a repo "
|
||||||
|
f"checkout?); set {_BUILD_RUST_EXTS_ENV}=none to build without "
|
||||||
|
"Rust extensions"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
out = subprocess.run(
|
||||||
|
[
|
||||||
|
"cargo",
|
||||||
|
"metadata",
|
||||||
|
"--format-version",
|
||||||
|
"1",
|
||||||
|
"--no-deps",
|
||||||
|
"--manifest-path",
|
||||||
|
str(manifest_path),
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise RuntimeError(
|
||||||
|
"cargo is required to discover the Rust extension modules in "
|
||||||
|
f"{_RUST_WORKSPACE_DIR} (and to build them); install a Rust "
|
||||||
|
f"toolchain, or set {_BUILD_RUST_EXTS_ENV}=none to build without "
|
||||||
|
"Rust extensions"
|
||||||
|
) from exc
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
raise RuntimeError(f"cargo metadata failed:\n{exc.stderr}") from exc
|
||||||
|
return json.loads(out.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def _match_by_substring(declared, tokens, source):
|
||||||
|
"""Match tokens as case-insensitive substrings of extension names."""
|
||||||
|
matched = set()
|
||||||
|
unmatched = []
|
||||||
|
for token in tokens:
|
||||||
|
hits = {ext.name for ext in declared if token in ext.name.lower()}
|
||||||
|
if hits:
|
||||||
|
matched |= hits
|
||||||
|
else:
|
||||||
|
unmatched.append(token)
|
||||||
|
if unmatched:
|
||||||
|
declared_names = sorted(ext.name for ext in declared)
|
||||||
|
raise ValueError(
|
||||||
|
f"{source} matched no discovered Rust extension for: {unmatched}; "
|
||||||
|
f"discovered extensions are {declared_names}"
|
||||||
|
)
|
||||||
|
return [ext for ext in declared if ext.name in matched]
|
||||||
|
|
||||||
|
|
||||||
|
def _discovered_rust_extensions():
|
||||||
|
"""One RustExtension per workspace crate declaring a python-module."""
|
||||||
|
extensions = []
|
||||||
|
for package in sorted(
|
||||||
|
_cargo_workspace_metadata()["packages"], key=lambda p: p["name"]
|
||||||
|
):
|
||||||
|
sglang_meta = (package["metadata"] or {}).get("sglang", {})
|
||||||
|
if "python-module" not in sglang_meta:
|
||||||
|
continue
|
||||||
|
extensions.append(
|
||||||
|
RustExtension(
|
||||||
|
target=sglang_meta["python-module"],
|
||||||
|
path=package["manifest_path"],
|
||||||
|
binding=Binding.PyO3,
|
||||||
|
debug=sglang_meta.get("debug"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not extensions:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"no crate under {_RUST_WORKSPACE_DIR} declares "
|
||||||
|
"[package.metadata.sglang] python-module; set "
|
||||||
|
f"{_BUILD_RUST_EXTS_ENV}=none to build without Rust extensions"
|
||||||
|
)
|
||||||
|
return extensions
|
||||||
|
|
||||||
|
|
||||||
|
# Deliberately not a TOML parser (keeps setup.py stdlib-only): the allowlist
|
||||||
|
# must be written as a single line, e.g. rust-extensions = ["multimodal"].
|
||||||
|
_ALLOWLIST_RE = re.compile(r"^rust-extensions\s*=\s*\[([^\]]*)\]", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
def _pyproject_rust_extensions(declared):
|
||||||
|
"""Apply the active pyproject's [tool.sglang] rust-extensions allowlist."""
|
||||||
|
pyproject_text = (_PYTHON_DIR / "pyproject.toml").read_text(encoding="utf-8")
|
||||||
|
match = _ALLOWLIST_RE.search(pyproject_text)
|
||||||
|
if match is None:
|
||||||
|
return declared
|
||||||
|
tokens = re.findall(r'"([^"]*)"', match.group(1))
|
||||||
|
return _match_by_substring(
|
||||||
|
declared=declared,
|
||||||
|
tokens=[token.lower() for token in tokens],
|
||||||
|
source="[tool.sglang] rust-extensions",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _selected_rust_extensions(declared):
|
def _selected_rust_extensions(declared):
|
||||||
"""Return the Rust extensions selected by SGLANG_BUILD_RUST_EXTS.
|
"""Apply the SGLANG_BUILD_RUST_EXTS build-time filter."""
|
||||||
|
|
||||||
`ext.name` is the fully-qualified target (e.g. "sglang.srt.grpc._core") for
|
|
||||||
the string-target declarations in pyproject.toml, so comma-separated names
|
|
||||||
are matched as case-insensitive substrings of it.
|
|
||||||
"""
|
|
||||||
declared = list(declared)
|
declared = list(declared)
|
||||||
raw = os.environ.get(_BUILD_RUST_EXTS_ENV)
|
raw = os.environ.get(_BUILD_RUST_EXTS_ENV)
|
||||||
if raw is None:
|
if raw is None:
|
||||||
@@ -52,23 +165,17 @@ def _selected_rust_extensions(declared):
|
|||||||
f"{_BUILD_RUST_EXTS_ENV}={raw!r} has an empty item; unset it or use "
|
f"{_BUILD_RUST_EXTS_ENV}={raw!r} has an empty item; unset it or use "
|
||||||
"'all', 'none', or a comma-separated list of extension names"
|
"'all', 'none', or a comma-separated list of extension names"
|
||||||
)
|
)
|
||||||
|
return _match_by_substring(
|
||||||
matched = set()
|
declared=declared, tokens=tokens, source=_BUILD_RUST_EXTS_ENV
|
||||||
unmatched = []
|
|
||||||
for token in tokens:
|
|
||||||
hits = {ext.name for ext in declared if token in ext.name.lower()}
|
|
||||||
if hits:
|
|
||||||
matched |= hits
|
|
||||||
else:
|
|
||||||
unmatched.append(token)
|
|
||||||
if unmatched:
|
|
||||||
declared_names = sorted(ext.name for ext in declared)
|
|
||||||
raise ValueError(
|
|
||||||
f"{_BUILD_RUST_EXTS_ENV} matched no declared Rust extension for: "
|
|
||||||
f"{unmatched}; declared extensions are {declared_names}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return [ext for ext in declared if ext.name in matched]
|
|
||||||
|
def _declared_rust_extensions():
|
||||||
|
# "none" short-circuits discovery so builds without a ../rust checkout
|
||||||
|
# (e.g. from an sdist) still work.
|
||||||
|
if (os.environ.get(_BUILD_RUST_EXTS_ENV) or "").strip().lower() == "none":
|
||||||
|
return []
|
||||||
|
return _pyproject_rust_extensions(_discovered_rust_extensions())
|
||||||
|
|
||||||
|
|
||||||
if build_rust is not None:
|
if build_rust is not None:
|
||||||
@@ -84,9 +191,9 @@ if build_rust is not None:
|
|||||||
return
|
return
|
||||||
super().run()
|
super().run()
|
||||||
|
|
||||||
_cmdclass = {"build_rust": BuildRust}
|
setup(
|
||||||
|
cmdclass={"build_rust": BuildRust},
|
||||||
|
rust_extensions=_declared_rust_extensions(),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
_cmdclass = {}
|
setup()
|
||||||
|
|
||||||
|
|
||||||
setup(cmdclass=_cmdclass)
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "3"
|
||||||
|
members = ["sglang-grpc", "sglang-mm"]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
# Single source of truth for dependency versions across workspace members.
|
||||||
|
# Members pull these in with `dep = { workspace = true }` (plus extra features
|
||||||
|
# where needed).
|
||||||
|
[workspace.dependencies]
|
||||||
|
async-stream = "0.3"
|
||||||
|
pyo3 = { version = "0.29.0", features = ["extension-module"] }
|
||||||
|
serde_json = "1"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tokio-stream = { version = "0.1", features = ["net"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
|
||||||
|
# Profiles only take effect at the workspace root; per-member profile sections
|
||||||
|
# are ignored by cargo. Per-package tweaks live in the override tables below.
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
strip = true
|
||||||
|
opt-level = 3
|
||||||
|
codegen-units = 1
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 0
|
||||||
|
debug = 1
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.90"
|
channel = "1.90"
|
||||||
profile = "minimal"
|
profile = "minimal"
|
||||||
|
components = ["clippy", "rustfmt"]
|
||||||
+26
-22
@@ -1,38 +1,42 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sglang-grpc"
|
name = "sglang-grpc"
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
description = "In-process Rust gRPC server for SGLang"
|
description = "In-process Rust gRPC server for SGLang"
|
||||||
license = "Apache-2.0"
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
# Consumed by python/setup.py: registers this crate as a PyO3 extension module
|
||||||
|
# of the main sglang wheel at the given import path.
|
||||||
|
[package.metadata.sglang]
|
||||||
|
python-module = "sglang.srt.grpc._core"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "_core"
|
# Unique per-crate artifact name (the shared workspace target/ dir cannot hold
|
||||||
|
# two `lib_core` cdylibs). The importable Python module is still `_core`: the
|
||||||
|
# name comes from the `#[pymodule]` entry point and the setuptools-rust
|
||||||
|
# `target` in python/pyproject.toml, which renames the built artifact.
|
||||||
|
name = "sglang_grpc_core"
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pyo3 = { version = "0.23", features = ["extension-module"] }
|
async-stream = { workspace = true }
|
||||||
tokio = { version = "1", features = ["full"] }
|
pyo3 = { workspace = true }
|
||||||
tonic = { version = "0.12", features = ["gzip", "transport"] }
|
serde_json = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
tokio-stream = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-subscriber = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
|
||||||
prost = "0.13"
|
prost = "0.13"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
|
||||||
tracing = "0.1"
|
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
||||||
serde_json = "1"
|
|
||||||
tokenizers = { version = "0.21", default-features = false, features = ["onig"] }
|
tokenizers = { version = "0.21", default-features = false, features = ["onig"] }
|
||||||
tokio-stream = { version = "0.1", features = ["net"] }
|
tonic = { version = "0.12", features = ["gzip", "transport"] }
|
||||||
async-stream = "0.3"
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tonic-build = "0.12"
|
tonic-build = "0.12"
|
||||||
|
# Fallback protoc for machines without a system install (see build.rs); keeps
|
||||||
|
# `cargo clippy/check/build` and lint CI working without apt/brew protobuf.
|
||||||
|
protoc-bin-vendored = "3"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["pyo3/extension-module"]
|
default = ["pyo3/extension-module"]
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
opt-level = 2
|
|
||||||
lto = "thin"
|
|
||||||
strip = true
|
|
||||||
|
|
||||||
[profile.dev]
|
|
||||||
opt-level = 0
|
|
||||||
debug = 1
|
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Prefer an explicitly configured protoc; otherwise use the vendored
|
||||||
|
// binary so builds (including `cargo clippy` and the pre-commit hook on
|
||||||
|
// machines/CI runners without protobuf installed) are self-contained.
|
||||||
|
// protoc_bin_path() errs on platforms the vendored crate doesn't cover;
|
||||||
|
// those fall back to prost-build's own lookup of `protoc` on PATH.
|
||||||
|
if std::env::var_os("PROTOC").is_none()
|
||||||
|
&& let Ok(vendored) = protoc_bin_vendored::protoc_bin_path()
|
||||||
|
{
|
||||||
|
// SAFETY: build scripts are single-threaded at this point.
|
||||||
|
unsafe { std::env::set_var("PROTOC", vendored) };
|
||||||
|
}
|
||||||
|
|
||||||
let proto_path = "../../proto/sglang/runtime/v1/sglang.proto";
|
let proto_path = "../../proto/sglang/runtime/v1/sglang.proto";
|
||||||
|
|
||||||
tonic_build::configure()
|
tonic_build::configure()
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Standalone dev builds only (`maturin build` / `maturin develop`); the
|
||||||
|
# extension ships to users inside the main sglang wheel via setuptools-rust
|
||||||
|
# (python/pyproject.toml, target sglang.srt.grpc._core). Requires a repo
|
||||||
|
# checkout (build.rs reads ../../proto), so this is not publishable as an
|
||||||
|
# sdist. protoc is not required: build.rs falls back to a vendored binary.
|
||||||
|
[build-system]
|
||||||
|
requires = ["maturin>=1.5,<2"]
|
||||||
|
build-backend = "maturin"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "sglang-grpc"
|
||||||
|
dynamic = ["version"]
|
||||||
|
description = "In-process Rust gRPC server for SGLang"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
|
[tool.maturin]
|
||||||
|
module-name = "_core"
|
||||||
@@ -40,7 +40,7 @@ type BridgeStateRef = Arc<Mutex<BridgeState>>;
|
|||||||
struct BridgeState {
|
struct BridgeState {
|
||||||
channels: HashMap<String, Sender<ResponseChunk>>,
|
channels: HashMap<String, Sender<ResponseChunk>>,
|
||||||
pending_sends: HashSet<String>,
|
pending_sends: HashSet<String>,
|
||||||
ready_callbacks: HashMap<String, PyObject>,
|
ready_callbacks: HashMap<String, Py<PyAny>>,
|
||||||
ready_signals: HashSet<String>,
|
ready_signals: HashSet<String>,
|
||||||
terminal_errors: HashMap<String, TerminalError>,
|
terminal_errors: HashMap<String, TerminalError>,
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,10 @@ impl TerminalError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pyclass(eq, eq_int)]
|
// skip_from_py_object: this enum is only returned to Python, never received
|
||||||
|
// from it, so it opts out of pyo3's (deprecated-by-default) FromPyObject
|
||||||
|
// derive for Clone pyclasses.
|
||||||
|
#[pyclass(eq, eq_int, skip_from_py_object)]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum ChunkSendStatus {
|
pub enum ChunkSendStatus {
|
||||||
Ready,
|
Ready,
|
||||||
@@ -83,7 +86,7 @@ fn lock_or_recover<'a, T>(mutex: &'a Mutex<T>, name: &'static str) -> MutexGuard
|
|||||||
|
|
||||||
/// Holds a reference to the Python RuntimeHandle and manages per-request channels.
|
/// Holds a reference to the Python RuntimeHandle and manages per-request channels.
|
||||||
pub struct PyBridge {
|
pub struct PyBridge {
|
||||||
runtime_handle: PyObject,
|
runtime_handle: Py<PyAny>,
|
||||||
state: BridgeStateRef,
|
state: BridgeStateRef,
|
||||||
rust_tokenizer: Option<RustTokenizer>,
|
rust_tokenizer: Option<RustTokenizer>,
|
||||||
context_len: i32,
|
context_len: i32,
|
||||||
@@ -93,7 +96,7 @@ pub struct PyBridge {
|
|||||||
|
|
||||||
impl PyBridge {
|
impl PyBridge {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
runtime_handle: PyObject,
|
runtime_handle: Py<PyAny>,
|
||||||
rust_tokenizer: Option<RustTokenizer>,
|
rust_tokenizer: Option<RustTokenizer>,
|
||||||
context_len: i32,
|
context_len: i32,
|
||||||
response_channel_capacity: usize,
|
response_channel_capacity: usize,
|
||||||
@@ -144,7 +147,7 @@ impl PyBridge {
|
|||||||
Ok(receiver)
|
Ok(receiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_chunk_callback(&self, py: Python<'_>, rid: String) -> PyResult<PyObject> {
|
fn make_chunk_callback(&self, py: Python<'_>, rid: String) -> PyResult<Py<PyAny>> {
|
||||||
let callback = ChunkCallback {
|
let callback = ChunkCallback {
|
||||||
rid,
|
rid,
|
||||||
state: self.state.clone(),
|
state: self.state.clone(),
|
||||||
@@ -155,7 +158,7 @@ impl PyBridge {
|
|||||||
Ok(py_callback.into_any())
|
Ok(py_callback.into_any())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_json_callback(&self, py: Python<'_>, rid: String) -> PyResult<PyObject> {
|
fn make_json_callback(&self, py: Python<'_>, rid: String) -> PyResult<Py<PyAny>> {
|
||||||
let callback = JsonChunkCallback {
|
let callback = JsonChunkCallback {
|
||||||
rid,
|
rid,
|
||||||
state: self.state.clone(),
|
state: self.state.clone(),
|
||||||
@@ -183,7 +186,7 @@ impl PyBridge {
|
|||||||
let receiver = self.create_channel(rid)?;
|
let receiver = self.create_channel(rid)?;
|
||||||
let rid_owned = rid.to_string();
|
let rid_owned = rid.to_string();
|
||||||
|
|
||||||
let result = Python::with_gil(|py| -> PyResult<()> {
|
let result = Python::attach(|py| -> PyResult<()> {
|
||||||
let py_req_dict = json_map_to_pydict(py, &req_dict)?;
|
let py_req_dict = json_map_to_pydict(py, &req_dict)?;
|
||||||
let callback = self.make_chunk_callback(py, rid_owned)?;
|
let callback = self.make_chunk_callback(py, rid_owned)?;
|
||||||
|
|
||||||
@@ -253,7 +256,7 @@ impl PyBridge {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
Python::with_gil(|py| {
|
Python::attach(|py| {
|
||||||
self.runtime_handle
|
self.runtime_handle
|
||||||
.call_method1(py, "abort", (rid, abort_all))?;
|
.call_method1(py, "abort", (rid, abort_all))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -265,21 +268,21 @@ impl PyBridge {
|
|||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
pub fn get_model_info(&self) -> PyResult<String> {
|
pub fn get_model_info(&self) -> PyResult<String> {
|
||||||
Python::with_gil(|py| {
|
Python::attach(|py| {
|
||||||
let result = self.runtime_handle.call_method0(py, "get_model_info")?;
|
let result = self.runtime_handle.call_method0(py, "get_model_info")?;
|
||||||
result.extract::<String>(py)
|
result.extract::<String>(py)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_server_info(&self) -> PyResult<String> {
|
pub fn get_server_info(&self) -> PyResult<String> {
|
||||||
Python::with_gil(|py| {
|
Python::attach(|py| {
|
||||||
let result = self.runtime_handle.call_method0(py, "get_server_info")?;
|
let result = self.runtime_handle.call_method0(py, "get_server_info")?;
|
||||||
result.extract::<String>(py)
|
result.extract::<String>(py)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn health_check(&self) -> PyResult<bool> {
|
pub fn health_check(&self) -> PyResult<bool> {
|
||||||
Python::with_gil(|py| {
|
Python::attach(|py| {
|
||||||
let result = self.runtime_handle.call_method0(py, "health_check")?;
|
let result = self.runtime_handle.call_method0(py, "health_check")?;
|
||||||
result.extract::<bool>(py)
|
result.extract::<bool>(py)
|
||||||
})
|
})
|
||||||
@@ -287,7 +290,7 @@ impl PyBridge {
|
|||||||
|
|
||||||
/// Tokenize via Python (fallback when Rust tokenizer unavailable).
|
/// Tokenize via Python (fallback when Rust tokenizer unavailable).
|
||||||
pub fn tokenize_py(&self, text: &str, add_special_tokens: bool) -> PyResult<String> {
|
pub fn tokenize_py(&self, text: &str, add_special_tokens: bool) -> PyResult<String> {
|
||||||
Python::with_gil(|py| {
|
Python::attach(|py| {
|
||||||
let result =
|
let result =
|
||||||
self.runtime_handle
|
self.runtime_handle
|
||||||
.call_method1(py, "tokenize", (text, add_special_tokens))?;
|
.call_method1(py, "tokenize", (text, add_special_tokens))?;
|
||||||
@@ -297,7 +300,7 @@ impl PyBridge {
|
|||||||
|
|
||||||
/// Detokenize via Python (fallback when Rust tokenizer unavailable).
|
/// Detokenize via Python (fallback when Rust tokenizer unavailable).
|
||||||
pub fn detokenize_py(&self, tokens: Vec<i32>) -> PyResult<String> {
|
pub fn detokenize_py(&self, tokens: Vec<i32>) -> PyResult<String> {
|
||||||
Python::with_gil(|py| {
|
Python::attach(|py| {
|
||||||
let result = self
|
let result = self
|
||||||
.runtime_handle
|
.runtime_handle
|
||||||
.call_method1(py, "detokenize", (tokens,))?;
|
.call_method1(py, "detokenize", (tokens,))?;
|
||||||
@@ -306,7 +309,7 @@ impl PyBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_models(&self) -> PyResult<String> {
|
pub fn list_models(&self) -> PyResult<String> {
|
||||||
Python::with_gil(|py| {
|
Python::attach(|py| {
|
||||||
let result = self.runtime_handle.call_method0(py, "list_models")?;
|
let result = self.runtime_handle.call_method0(py, "list_models")?;
|
||||||
result.extract::<String>(py)
|
result.extract::<String>(py)
|
||||||
})
|
})
|
||||||
@@ -314,13 +317,13 @@ impl PyBridge {
|
|||||||
|
|
||||||
fn submit_json<F>(&self, rid: &str, call: F) -> PyResult<Receiver<ResponseChunk>>
|
fn submit_json<F>(&self, rid: &str, call: F) -> PyResult<Receiver<ResponseChunk>>
|
||||||
where
|
where
|
||||||
F: for<'py> FnOnce(Python<'py>, &PyObject, PyObject) -> PyResult<()>,
|
F: for<'py> FnOnce(Python<'py>, &Py<PyAny>, Py<PyAny>) -> PyResult<()>,
|
||||||
{
|
{
|
||||||
// Closure args are: current Python token, RuntimeHandle, and the JSON chunk callback.
|
// Closure args are: current Python token, RuntimeHandle, and the JSON chunk callback.
|
||||||
let receiver = self.create_channel(rid)?;
|
let receiver = self.create_channel(rid)?;
|
||||||
let rid_owned = rid.to_string();
|
let rid_owned = rid.to_string();
|
||||||
|
|
||||||
let result = Python::with_gil(|py| -> PyResult<()> {
|
let result = Python::attach(|py| -> PyResult<()> {
|
||||||
let callback = self.make_json_callback(py, rid_owned)?;
|
let callback = self.make_json_callback(py, rid_owned)?;
|
||||||
call(py, &self.runtime_handle, callback)
|
call(py, &self.runtime_handle, callback)
|
||||||
});
|
});
|
||||||
@@ -450,7 +453,7 @@ fn close_channel_with_error(
|
|||||||
py: Python<'_>,
|
py: Python<'_>,
|
||||||
rid: &str,
|
rid: &str,
|
||||||
state: &BridgeStateRef,
|
state: &BridgeStateRef,
|
||||||
runtime_handle: &PyObject,
|
runtime_handle: &Py<PyAny>,
|
||||||
error: TerminalError,
|
error: TerminalError,
|
||||||
) {
|
) {
|
||||||
let mut state = lock_or_recover(state.as_ref(), "state");
|
let mut state = lock_or_recover(state.as_ref(), "state");
|
||||||
@@ -478,7 +481,7 @@ fn register_pending_send(rid: &str, state: &BridgeStateRef) -> bool {
|
|||||||
state.pending_sends.insert(rid.to_string())
|
state.pending_sends.insert(rid.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mark_send_ready(py: Python<'_>, rid: &str, state: &BridgeStateRef) -> Option<PyObject> {
|
fn mark_send_ready(py: Python<'_>, rid: &str, state: &BridgeStateRef) -> Option<Py<PyAny>> {
|
||||||
let mut state = lock_or_recover(state.as_ref(), "state");
|
let mut state = lock_or_recover(state.as_ref(), "state");
|
||||||
state.pending_sends.remove(rid);
|
state.pending_sends.remove(rid);
|
||||||
if let Some(callback) = state.ready_callbacks.get(rid) {
|
if let Some(callback) = state.ready_callbacks.get(rid) {
|
||||||
@@ -489,7 +492,7 @@ fn mark_send_ready(py: Python<'_>, rid: &str, state: &BridgeStateRef) -> Option<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notify_ready(py: Python<'_>, rid: &str, callback: PyObject) {
|
fn notify_ready(py: Python<'_>, rid: &str, callback: Py<PyAny>) {
|
||||||
if let Err(err) = callback.call0(py) {
|
if let Err(err) = callback.call0(py) {
|
||||||
tracing::warn!(rid, "gRPC on_ready callback failed: {}", err);
|
tracing::warn!(rid, "gRPC on_ready callback failed: {}", err);
|
||||||
}
|
}
|
||||||
@@ -499,7 +502,7 @@ fn set_on_ready_for_rid(
|
|||||||
py: Python<'_>,
|
py: Python<'_>,
|
||||||
rid: &str,
|
rid: &str,
|
||||||
state: &BridgeStateRef,
|
state: &BridgeStateRef,
|
||||||
on_ready: PyObject,
|
on_ready: Py<PyAny>,
|
||||||
) -> PyResult<()> {
|
) -> PyResult<()> {
|
||||||
let should_notify = {
|
let should_notify = {
|
||||||
let mut state = lock_or_recover(state.as_ref(), "state");
|
let mut state = lock_or_recover(state.as_ref(), "state");
|
||||||
@@ -525,7 +528,7 @@ fn try_send_chunk(
|
|||||||
py: Python<'_>,
|
py: Python<'_>,
|
||||||
rid: &str,
|
rid: &str,
|
||||||
state: &BridgeStateRef,
|
state: &BridgeStateRef,
|
||||||
runtime_handle: &PyObject,
|
runtime_handle: &Py<PyAny>,
|
||||||
tokio_handle: &Handle,
|
tokio_handle: &Handle,
|
||||||
sender: &Sender<ResponseChunk>,
|
sender: &Sender<ResponseChunk>,
|
||||||
msg: ResponseChunk,
|
msg: ResponseChunk,
|
||||||
@@ -569,14 +572,14 @@ fn try_send_chunk(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Python::with_gil(|py| {
|
Python::attach(|py| {
|
||||||
if let Some(callback) = mark_send_ready(py, &rid_owned, &state) {
|
if let Some(callback) = mark_send_ready(py, &rid_owned, &state) {
|
||||||
notify_ready(py, &rid_owned, callback);
|
notify_ready(py, &rid_owned, callback);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
Python::with_gil(|py| {
|
Python::attach(|py| {
|
||||||
close_channel_with_error(
|
close_channel_with_error(
|
||||||
py,
|
py,
|
||||||
&rid_owned,
|
&rid_owned,
|
||||||
@@ -611,7 +614,7 @@ fn try_send_chunk(
|
|||||||
struct ChunkCallback {
|
struct ChunkCallback {
|
||||||
rid: String,
|
rid: String,
|
||||||
state: BridgeStateRef,
|
state: BridgeStateRef,
|
||||||
runtime_handle: PyObject,
|
runtime_handle: Py<PyAny>,
|
||||||
tokio_handle: Handle,
|
tokio_handle: Handle,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -619,7 +622,7 @@ struct ChunkCallback {
|
|||||||
impl ChunkCallback {
|
impl ChunkCallback {
|
||||||
/// Register before producing chunks. If a parked chunk drained before registration,
|
/// Register before producing chunks. If a parked chunk drained before registration,
|
||||||
/// Rust fires `on_ready` immediately so late registration cannot miss the edge.
|
/// Rust fires `on_ready` immediately so late registration cannot miss the edge.
|
||||||
fn set_on_ready(&self, py: Python<'_>, on_ready: PyObject) -> PyResult<()> {
|
fn set_on_ready(&self, py: Python<'_>, on_ready: Py<PyAny>) -> PyResult<()> {
|
||||||
set_on_ready_for_rid(py, &self.rid, &self.state, on_ready)
|
set_on_ready_for_rid(py, &self.rid, &self.state, on_ready)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,7 +702,7 @@ impl ChunkCallback {
|
|||||||
struct JsonChunkCallback {
|
struct JsonChunkCallback {
|
||||||
rid: String,
|
rid: String,
|
||||||
state: BridgeStateRef,
|
state: BridgeStateRef,
|
||||||
runtime_handle: PyObject,
|
runtime_handle: Py<PyAny>,
|
||||||
tokio_handle: Handle,
|
tokio_handle: Handle,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -707,7 +710,7 @@ struct JsonChunkCallback {
|
|||||||
impl JsonChunkCallback {
|
impl JsonChunkCallback {
|
||||||
/// Register before producing chunks. If a parked chunk drained before registration,
|
/// Register before producing chunks. If a parked chunk drained before registration,
|
||||||
/// Rust fires `on_ready` immediately so late registration cannot miss the edge.
|
/// Rust fires `on_ready` immediately so late registration cannot miss the edge.
|
||||||
fn set_on_ready(&self, py: Python<'_>, on_ready: PyObject) -> PyResult<()> {
|
fn set_on_ready(&self, py: Python<'_>, on_ready: Py<PyAny>) -> PyResult<()> {
|
||||||
set_on_ready_for_rid(py, &self.rid, &self.state, on_ready)
|
set_on_ready_for_rid(py, &self.rid, &self.state, on_ready)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -785,7 +788,7 @@ impl JsonChunkCallback {
|
|||||||
fn extract_meta_info(chunk: &Bound<'_, PyDict>) -> HashMap<String, String> {
|
fn extract_meta_info(chunk: &Bound<'_, PyDict>) -> HashMap<String, String> {
|
||||||
let mut meta = HashMap::new();
|
let mut meta = HashMap::new();
|
||||||
if let Ok(Some(meta_obj)) = chunk.get_item("meta_info")
|
if let Ok(Some(meta_obj)) = chunk.get_item("meta_info")
|
||||||
&& let Ok(meta_dict) = meta_obj.downcast::<PyDict>()
|
&& let Ok(meta_dict) = meta_obj.cast::<PyDict>()
|
||||||
{
|
{
|
||||||
for (k, v) in meta_dict.iter() {
|
for (k, v) in meta_dict.iter() {
|
||||||
// The proto schema is map<string, string>; encode each Python value as JSON
|
// The proto schema is map<string, string>; encode each Python value as JSON
|
||||||
|
|||||||
@@ -53,10 +53,10 @@ struct TokenizerInfo {
|
|||||||
/// fall back to Python tokenization.
|
/// fall back to Python tokenization.
|
||||||
fn try_get_attr(
|
fn try_get_attr(
|
||||||
py: Python<'_>,
|
py: Python<'_>,
|
||||||
obj: &PyObject,
|
obj: &Py<PyAny>,
|
||||||
attr: &'static str,
|
attr: &'static str,
|
||||||
context: &'static str,
|
context: &'static str,
|
||||||
) -> Option<PyObject> {
|
) -> Option<Py<PyAny>> {
|
||||||
obj.getattr(py, attr).map(Some).unwrap_or_else(|err| {
|
obj.getattr(py, attr).map(Some).unwrap_or_else(|err| {
|
||||||
tracing::debug!("{}.{} is unavailable: {}", context, attr, err);
|
tracing::debug!("{}.{} is unavailable: {}", context, attr, err);
|
||||||
None
|
None
|
||||||
@@ -65,7 +65,7 @@ fn try_get_attr(
|
|||||||
|
|
||||||
fn try_get_attr_str(
|
fn try_get_attr_str(
|
||||||
py: Python<'_>,
|
py: Python<'_>,
|
||||||
obj: &PyObject,
|
obj: &Py<PyAny>,
|
||||||
attr: &'static str,
|
attr: &'static str,
|
||||||
context: &'static str,
|
context: &'static str,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
@@ -79,7 +79,7 @@ fn try_get_attr_str(
|
|||||||
|
|
||||||
fn try_get_attr_i32(
|
fn try_get_attr_i32(
|
||||||
py: Python<'_>,
|
py: Python<'_>,
|
||||||
obj: &PyObject,
|
obj: &Py<PyAny>,
|
||||||
attr: &'static str,
|
attr: &'static str,
|
||||||
context: &'static str,
|
context: &'static str,
|
||||||
) -> Option<i32> {
|
) -> Option<i32> {
|
||||||
@@ -91,8 +91,8 @@ fn try_get_attr_i32(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_tokenizer_info(runtime_handle: &PyObject) -> PyResult<TokenizerInfo> {
|
fn extract_tokenizer_info(runtime_handle: &Py<PyAny>) -> PyResult<TokenizerInfo> {
|
||||||
Python::with_gil(|py| {
|
Python::attach(|py| {
|
||||||
let tm = runtime_handle
|
let tm = runtime_handle
|
||||||
.getattr(py, "tokenizer_manager")
|
.getattr(py, "tokenizer_manager")
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
@@ -152,7 +152,7 @@ fn extract_tokenizer_info(runtime_handle: &PyObject) -> PyResult<TokenizerInfo>
|
|||||||
fn start_server(
|
fn start_server(
|
||||||
host: String,
|
host: String,
|
||||||
port: u16,
|
port: u16,
|
||||||
runtime_handle: PyObject,
|
runtime_handle: Py<PyAny>,
|
||||||
worker_threads: usize,
|
worker_threads: usize,
|
||||||
response_channel_capacity: usize,
|
response_channel_capacity: usize,
|
||||||
response_timeout_secs: u64,
|
response_timeout_secs: u64,
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ fn resolve_max_message_size() -> usize {
|
|||||||
/// Everything else (typically `PyRuntimeError`, but also Python tracebacks
|
/// Everything else (typically `PyRuntimeError`, but also Python tracebacks
|
||||||
/// from inside the tokenizer manager) maps to `INTERNAL`.
|
/// from inside the tokenizer manager) maps to `INTERNAL`.
|
||||||
fn pyerr_to_status(err: PyErr, context: &str) -> Status {
|
fn pyerr_to_status(err: PyErr, context: &str) -> Status {
|
||||||
let is_client_error = Python::with_gil(|py| {
|
let is_client_error = Python::attach(|py| {
|
||||||
err.is_instance_of::<PyValueError>(py) || err.is_instance_of::<PyTypeError>(py)
|
err.is_instance_of::<PyValueError>(py) || err.is_instance_of::<PyTypeError>(py)
|
||||||
});
|
});
|
||||||
let msg = format!("{}: {}", context, err);
|
let msg = format!("{}: {}", context, err);
|
||||||
@@ -125,9 +125,10 @@ impl Drop for RequestAbortGuard {
|
|||||||
fn spawn_abort(bridge: Arc<PyBridge>, rid: String) {
|
fn spawn_abort(bridge: Arc<PyBridge>, rid: String) {
|
||||||
match tokio::runtime::Handle::try_current() {
|
match tokio::runtime::Handle::try_current() {
|
||||||
Ok(handle) => {
|
Ok(handle) => {
|
||||||
let _ = handle.spawn_blocking(move || {
|
// Fire-and-forget: dropping the JoinHandle detaches the task.
|
||||||
|
drop(handle.spawn_blocking(move || {
|
||||||
let _ = bridge.abort(&rid, false);
|
let _ = bridge.abort(&rid, false);
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use pyo3::prelude::*;
|
|||||||
use pyo3::types::{PyAny, PyDict, PyList};
|
use pyo3::types::{PyAny, PyDict, PyList};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
fn json_value_to_py<'py>(py: Python<'py>, v: &serde_json::Value) -> PyResult<PyObject> {
|
fn json_value_to_py<'py>(py: Python<'py>, v: &serde_json::Value) -> PyResult<Py<PyAny>> {
|
||||||
match v {
|
match v {
|
||||||
serde_json::Value::Null => Ok(py.None()),
|
serde_json::Value::Null => Ok(py.None()),
|
||||||
serde_json::Value::Bool(b) => Ok(b.into_pyobject(py)?.to_owned().into_any().unbind()),
|
serde_json::Value::Bool(b) => Ok(b.into_pyobject(py)?.to_owned().into_any().unbind()),
|
||||||
@@ -17,7 +17,7 @@ fn json_value_to_py<'py>(py: Python<'py>, v: &serde_json::Value) -> PyResult<PyO
|
|||||||
}
|
}
|
||||||
serde_json::Value::String(s) => Ok(s.into_pyobject(py)?.into_any().unbind()),
|
serde_json::Value::String(s) => Ok(s.into_pyobject(py)?.into_any().unbind()),
|
||||||
serde_json::Value::Array(arr) => {
|
serde_json::Value::Array(arr) => {
|
||||||
let items: Vec<PyObject> = arr
|
let items: Vec<Py<PyAny>> = arr
|
||||||
.iter()
|
.iter()
|
||||||
.map(|item| json_value_to_py(py, item))
|
.map(|item| json_value_to_py(py, item))
|
||||||
.collect::<PyResult<_>>()?;
|
.collect::<PyResult<_>>()?;
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
/target
|
|
||||||
+21
-13
@@ -1,23 +1,31 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sglang-mm"
|
name = "sglang-mm"
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
description = "Rust-accelerated multimodal preprocessing for SGLang"
|
description = "Rust-accelerated multimodal preprocessing for SGLang"
|
||||||
license = "Apache-2.0"
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
# Consumed by python/setup.py: registers this crate as a PyO3 extension module
|
||||||
|
# of the main sglang wheel at the given import path. debug = false keeps
|
||||||
|
# editable installs on release builds (image preprocessing is perf-sensitive).
|
||||||
|
[package.metadata.sglang]
|
||||||
|
python-module = "sglang.srt.multimodal._core"
|
||||||
|
debug = false
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "_core"
|
# Unique per-crate artifact name (the shared workspace target/ dir cannot hold
|
||||||
|
# two `lib_core` cdylibs). The importable Python module is still `_core`: the
|
||||||
|
# name comes from the `#[pymodule]` entry point and the setuptools-rust
|
||||||
|
# `target` in python/pyproject.toml, which renames the built artifact.
|
||||||
|
name = "sglang_mm_core"
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pyo3 = { version = "0.23", features = ["extension-module"] }
|
pyo3 = { workspace = true }
|
||||||
numpy = "0.23"
|
|
||||||
rayon = "1.10"
|
base64 = "0.22"
|
||||||
|
blake3 = "1"
|
||||||
half = "2.4"
|
half = "2.4"
|
||||||
image = { version = "0.25", default-features = false, features = ["jpeg", "png"] }
|
image = { version = "0.25", default-features = false, features = ["jpeg", "png"] }
|
||||||
blake3 = "1"
|
numpy = "0.29"
|
||||||
base64 = "0.22"
|
rayon = "1.10"
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ build-backend = "maturin"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "sglang-mm"
|
name = "sglang-mm"
|
||||||
version = "0.1.0"
|
dynamic = ["version"]
|
||||||
description = "Rust-accelerated multimodal preprocessing for SGLang"
|
description = "Rust-accelerated multimodal preprocessing for SGLang"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
[tool.maturin]
|
[tool.maturin]
|
||||||
module-name = "_core"
|
module-name = "_core"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use numpy::{IntoPyArray, PyArray1, PyReadonlyArray3, PyUntypedArrayMethods};
|
|||||||
use pyo3::exceptions::PyValueError;
|
use pyo3::exceptions::PyValueError;
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
|
||||||
pub fn pool() -> &'static rayon::ThreadPool {
|
pub fn pool() -> &'static rayon::ThreadPool {
|
||||||
static POOL: OnceLock<rayon::ThreadPool> = OnceLock::new();
|
static POOL: OnceLock<rayon::ThreadPool> = OnceLock::new();
|
||||||
POOL.get_or_init(|| {
|
POOL.get_or_init(|| {
|
||||||
@@ -72,10 +71,9 @@ pub fn resize_rgb<'py>(
|
|||||||
.as_slice()
|
.as_slice()
|
||||||
.map_err(|_| PyValueError::new_err("array must be C-contiguous"))?
|
.map_err(|_| PyValueError::new_err("array must be C-contiguous"))?
|
||||||
.to_vec();
|
.to_vec();
|
||||||
let out = py.allow_threads(move || {
|
let out =
|
||||||
pool().install(|| resize::resize_lanczos_rgb(&data, h, w, out_h, out_w))
|
py.detach(move || pool().install(|| resize::resize_lanczos_rgb(&data, h, w, out_h, out_w)));
|
||||||
});
|
Ok(out.into_pyarray(py))
|
||||||
Ok(out.into_pyarray_bound(py))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
@@ -95,14 +93,14 @@ pub fn image_decode_rgb<'py>(
|
|||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
) -> PyResult<(usize, usize, Bound<'py, PyArray1<u8>>)> {
|
) -> PyResult<(usize, usize, Bound<'py, PyArray1<u8>>)> {
|
||||||
let (rgb, h, w) = py
|
let (rgb, h, w) = py
|
||||||
.allow_threads(move || decode_rgb(&data))
|
.detach(move || decode_rgb(&data))
|
||||||
.map_err(PyValueError::new_err)?;
|
.map_err(PyValueError::new_err)?;
|
||||||
Ok((h, w, rgb.into_pyarray_bound(py)))
|
Ok((h, w, rgb.into_pyarray(py)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
pub fn data_hash(py: Python<'_>, data: Vec<u8>) -> u64 {
|
pub fn data_hash(py: Python<'_>, data: Vec<u8>) -> u64 {
|
||||||
py.allow_threads(move || {
|
py.detach(move || {
|
||||||
let digest = blake3::hash(&data);
|
let digest = blake3::hash(&data);
|
||||||
u64::from_be_bytes(digest.as_bytes()[..8].try_into().unwrap())
|
u64::from_be_bytes(digest.as_bytes()[..8].try_into().unwrap())
|
||||||
})
|
})
|
||||||
@@ -115,17 +113,17 @@ pub fn base64_decode<'py>(
|
|||||||
) -> PyResult<Bound<'py, pyo3::types::PyBytes>> {
|
) -> PyResult<Bound<'py, pyo3::types::PyBytes>> {
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
let decoded = py
|
let decoded = py
|
||||||
.allow_threads(|| {
|
.detach(|| {
|
||||||
base64::engine::general_purpose::STANDARD
|
base64::engine::general_purpose::STANDARD
|
||||||
.decode(encoded)
|
.decode(encoded)
|
||||||
.map_err(|e| format!("base64 decode error: {e}"))
|
.map_err(|e| format!("base64 decode error: {e}"))
|
||||||
})
|
})
|
||||||
.map_err(PyValueError::new_err)?;
|
.map_err(PyValueError::new_err)?;
|
||||||
Ok(pyo3::types::PyBytes::new_bound(py, &decoded))
|
Ok(pyo3::types::PyBytes::new(py, &decoded))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(parent: &Bound<'_, PyModule>) -> PyResult<()> {
|
pub fn register(parent: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||||
let m = PyModule::new_bound(parent.py(), "common")?;
|
let m = PyModule::new(parent.py(), "common")?;
|
||||||
m.add_function(wrap_pyfunction!(resize_rgb, &m)?)?;
|
m.add_function(wrap_pyfunction!(resize_rgb, &m)?)?;
|
||||||
m.add_function(wrap_pyfunction!(scaled_dims, &m)?)?;
|
m.add_function(wrap_pyfunction!(scaled_dims, &m)?)?;
|
||||||
m.add_function(wrap_pyfunction!(image_decode_rgb, &m)?)?;
|
m.add_function(wrap_pyfunction!(image_decode_rgb, &m)?)?;
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ fn precompute_coeffs(in_size: usize, out_size: usize) -> Coeffs {
|
|||||||
let count = (xmax - xmin) as usize;
|
let count = (xmax - xmin) as usize;
|
||||||
let k = &mut kkf[xx * ksize..(xx + 1) * ksize];
|
let k = &mut kkf[xx * ksize..(xx + 1) * ksize];
|
||||||
let mut ww = 0.0f64;
|
let mut ww = 0.0f64;
|
||||||
for x in 0..count {
|
for (x, kv) in k[..count].iter_mut().enumerate() {
|
||||||
let w = lanczos((x as f64 + xmin as f64 - center + 0.5) * ss);
|
let w = lanczos((x as f64 + xmin as f64 - center + 0.5) * ss);
|
||||||
k[x] = w;
|
*kv = w;
|
||||||
ww += w;
|
ww += w;
|
||||||
}
|
}
|
||||||
if ww != 0.0 {
|
if ww != 0.0 {
|
||||||
for x in 0..count {
|
for kv in k[..count].iter_mut() {
|
||||||
k[x] /= ww;
|
*kv /= ww;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bounds[xx] = (xmin as usize, count);
|
bounds[xx] = (xmin as usize, count);
|
||||||
@@ -111,9 +111,7 @@ fn resample_horizontal(src: &[u8], h: usize, w: usize, out_w: usize, c: &Coeffs)
|
|||||||
|
|
||||||
fn resample_vertical(src: &[u8], w: usize, out_h: usize, c: &Coeffs) -> Vec<u8> {
|
fn resample_vertical(src: &[u8], w: usize, out_h: usize, c: &Coeffs) -> Vec<u8> {
|
||||||
let mut out = vec![0u8; out_h * w * 3];
|
let mut out = vec![0u8; out_h * w * 3];
|
||||||
out.par_chunks_mut(w * 3)
|
out.par_chunks_mut(w * 3).enumerate().for_each(|(yy, row)| {
|
||||||
.enumerate()
|
|
||||||
.for_each(|(yy, row)| {
|
|
||||||
let (ymin, count) = c.bounds[yy];
|
let (ymin, count) = c.bounds[yy];
|
||||||
let k = &c.kk[yy * c.ksize..yy * c.ksize + count];
|
let k = &c.kk[yy * c.ksize..yy * c.ksize + count];
|
||||||
for x in 0..w {
|
for x in 0..w {
|
||||||
@@ -133,13 +131,7 @@ fn resample_vertical(src: &[u8], w: usize, out_h: usize, c: &Coeffs) -> Vec<u8>
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize_lanczos_rgb(
|
pub fn resize_lanczos_rgb(src: &[u8], h: usize, w: usize, out_h: usize, out_w: usize) -> Vec<u8> {
|
||||||
src: &[u8],
|
|
||||||
h: usize,
|
|
||||||
w: usize,
|
|
||||||
out_h: usize,
|
|
||||||
out_w: usize,
|
|
||||||
) -> Vec<u8> {
|
|
||||||
let need_h = out_w != w;
|
let need_h = out_w != w;
|
||||||
let need_v = out_h != h;
|
let need_v = out_h != h;
|
||||||
if need_h && need_v {
|
if need_h && need_v {
|
||||||
@@ -158,12 +150,7 @@ pub fn resize_lanczos_rgb(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scaled_dims(
|
pub fn scaled_dims(w: usize, h: usize, frac: Option<f64>, cap: Option<i64>) -> (usize, usize) {
|
||||||
w: usize,
|
|
||||||
h: usize,
|
|
||||||
frac: Option<f64>,
|
|
||||||
cap: Option<i64>,
|
|
||||||
) -> (usize, usize) {
|
|
||||||
let Some(frac) = frac else {
|
let Some(frac) = frac else {
|
||||||
return (w, h);
|
return (w, h);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
//!
|
//!
|
||||||
//! Model-specific processors compose these to build their preprocessing
|
//! Model-specific processors compose these to build their preprocessing
|
||||||
//! pipelines. All functions operate on flat RGB byte arrays (HWC layout).
|
//! pipelines. All functions operate on flat RGB byte arrays (HWC layout).
|
||||||
|
//!
|
||||||
|
//! Not every primitive is wired into a compiled-in processor yet; they are
|
||||||
|
//! kept available for upcoming model integrations.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
/// Normalize u8 RGB pixels to f32 in a single pass: `(pixel/255 - mean) / std`.
|
/// Normalize u8 RGB pixels to f32 in a single pass: `(pixel/255 - mean) / std`.
|
||||||
///
|
///
|
||||||
@@ -37,8 +41,8 @@ pub fn pad_to_grid(
|
|||||||
grid_w: usize,
|
grid_w: usize,
|
||||||
pad_value: &[f32],
|
pad_value: &[f32],
|
||||||
) -> (Vec<f32>, usize, usize) {
|
) -> (Vec<f32>, usize, usize) {
|
||||||
let new_h = ((h + grid_h - 1) / grid_h) * grid_h;
|
let new_h = h.div_ceil(grid_h) * grid_h;
|
||||||
let new_w = ((w + grid_w - 1) / grid_w) * grid_w;
|
let new_w = w.div_ceil(grid_w) * grid_w;
|
||||||
let mut out = vec![0.0f32; new_h * new_w * channels];
|
let mut out = vec![0.0f32; new_h * new_w * channels];
|
||||||
// Fill with pad value
|
// Fill with pad value
|
||||||
for i in 0..new_h * new_w {
|
for i in 0..new_h * new_w {
|
||||||
@@ -89,5 +93,5 @@ pub fn extract_patches_hwc(
|
|||||||
/// Compute the patch grid dimensions for a given image size and patch size.
|
/// Compute the patch grid dimensions for a given image size and patch size.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn patch_grid(h: usize, w: usize, patch_h: usize, patch_w: usize) -> (usize, usize) {
|
pub fn patch_grid(h: usize, w: usize, patch_h: usize, patch_w: usize) -> (usize, usize) {
|
||||||
((h + patch_h - 1) / patch_h, (w + patch_w - 1) / patch_w)
|
(h.div_ceil(patch_h), w.div_ceil(patch_w))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ use rayon::prelude::*;
|
|||||||
|
|
||||||
use crate::common;
|
use crate::common;
|
||||||
|
|
||||||
|
/// `(height, width, patches_as_u16_bits)` for one decoded image.
|
||||||
|
type Patches = (usize, usize, Vec<u16>);
|
||||||
|
/// [`Patches`] plus the image content hash.
|
||||||
|
type HashedPatches = (usize, usize, Vec<u16>, u64);
|
||||||
|
/// [`Patches`] with the patch data as a numpy array bound to `'py`.
|
||||||
|
type PyPatches<'py> = (usize, usize, Bound<'py, PyArray1<u16>>);
|
||||||
|
/// [`PyPatches`] plus the image content hash.
|
||||||
|
type PyHashedPatches<'py> = (usize, usize, Bound<'py, PyArray1<u16>>, u64);
|
||||||
|
|
||||||
const MEAN: [f32; 3] = [
|
const MEAN: [f32; 3] = [
|
||||||
0.48145466f64 as f32,
|
0.48145466f64 as f32,
|
||||||
0.4578275f64 as f32,
|
0.4578275f64 as f32,
|
||||||
@@ -40,7 +49,7 @@ fn luts() -> &'static [[u16; 256]; 3] {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn grid(h: usize, w: usize, ps: usize) -> (usize, usize) {
|
pub fn grid(h: usize, w: usize, ps: usize) -> (usize, usize) {
|
||||||
((h + ps - 1) / ps, w / ps + 1)
|
(h.div_ceil(ps), w / ps + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn patchify_into(arr: &[u8], h: usize, w: usize, ps: usize, out: &mut [u16]) {
|
fn patchify_into(arr: &[u8], h: usize, w: usize, ps: usize, out: &mut [u16]) {
|
||||||
@@ -95,7 +104,9 @@ fn patchify_alloc(arr: &[u8], h: usize, w: usize, ps: usize) -> Vec<u16> {
|
|||||||
|
|
||||||
fn check_ps(ps: usize) -> PyResult<()> {
|
fn check_ps(ps: usize) -> PyResult<()> {
|
||||||
if ps == 0 {
|
if ps == 0 {
|
||||||
return Err(PyValueError::new_err("patch_size must be greater than zero"));
|
return Err(PyValueError::new_err(
|
||||||
|
"patch_size must be greater than zero",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -118,8 +129,8 @@ fn patchify_rgb<'py>(
|
|||||||
.as_slice()
|
.as_slice()
|
||||||
.map_err(|_| PyValueError::new_err("array must be C-contiguous"))?
|
.map_err(|_| PyValueError::new_err("array must be C-contiguous"))?
|
||||||
.to_vec();
|
.to_vec();
|
||||||
let out = py.allow_threads(move || patchify_alloc(&data, h, w, patch_size));
|
let out = py.detach(move || patchify_alloc(&data, h, w, patch_size));
|
||||||
Ok(out.into_pyarray_bound(py))
|
Ok(out.into_pyarray(py))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
@@ -133,14 +144,14 @@ fn decode_patchify<'py>(
|
|||||||
) -> PyResult<(usize, usize, Bound<'py, PyArray1<u16>>)> {
|
) -> PyResult<(usize, usize, Bound<'py, PyArray1<u16>>)> {
|
||||||
check_ps(patch_size)?;
|
check_ps(patch_size)?;
|
||||||
let (h, w, out) = py
|
let (h, w, out) = py
|
||||||
.allow_threads(move || {
|
.detach(move || {
|
||||||
common::pool().install(|| {
|
common::pool().install(|| {
|
||||||
let (rgb, h, w) = common::decode_rescale(&data, rescale_frac, rescale_cap)?;
|
let (rgb, h, w) = common::decode_rescale(&data, rescale_frac, rescale_cap)?;
|
||||||
Ok::<_, String>((h, w, patchify_alloc(&rgb, h, w, patch_size)))
|
Ok::<_, String>((h, w, patchify_alloc(&rgb, h, w, patch_size)))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.map_err(PyValueError::new_err)?;
|
.map_err(PyValueError::new_err)?;
|
||||||
Ok((h, w, out.into_pyarray_bound(py)))
|
Ok((h, w, out.into_pyarray(py)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
@@ -151,10 +162,9 @@ fn decode_patchify_batch<'py>(
|
|||||||
patch_size: usize,
|
patch_size: usize,
|
||||||
rescale_frac: Option<f64>,
|
rescale_frac: Option<f64>,
|
||||||
rescale_cap: Option<i64>,
|
rescale_cap: Option<i64>,
|
||||||
) -> PyResult<Vec<(usize, usize, Bound<'py, PyArray1<u16>>)>> {
|
) -> PyResult<Vec<PyPatches<'py>>> {
|
||||||
check_ps(patch_size)?;
|
check_ps(patch_size)?;
|
||||||
let results: Vec<Result<(usize, usize, Vec<u16>), String>> =
|
let results: Vec<Result<Patches, String>> = py.detach(move || {
|
||||||
py.allow_threads(move || {
|
|
||||||
common::pool().install(|| {
|
common::pool().install(|| {
|
||||||
datas
|
datas
|
||||||
.par_iter()
|
.par_iter()
|
||||||
@@ -169,7 +179,7 @@ fn decode_patchify_batch<'py>(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
let (h, w, v) = r.map_err(PyValueError::new_err)?;
|
let (h, w, v) = r.map_err(PyValueError::new_err)?;
|
||||||
Ok((h, w, v.into_pyarray_bound(py)))
|
Ok((h, w, v.into_pyarray(py)))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -182,10 +192,9 @@ fn preprocess_images<'py>(
|
|||||||
patch_size: usize,
|
patch_size: usize,
|
||||||
rescale_frac: Option<f64>,
|
rescale_frac: Option<f64>,
|
||||||
rescale_cap: Option<i64>,
|
rescale_cap: Option<i64>,
|
||||||
) -> PyResult<Vec<(usize, usize, Bound<'py, PyArray1<u16>>, u64)>> {
|
) -> PyResult<Vec<PyHashedPatches<'py>>> {
|
||||||
check_ps(patch_size)?;
|
check_ps(patch_size)?;
|
||||||
let results: Vec<Result<(usize, usize, Vec<u16>, u64), String>> =
|
let results: Vec<Result<HashedPatches, String>> = py.detach(move || {
|
||||||
py.allow_threads(move || {
|
|
||||||
common::pool().install(|| {
|
common::pool().install(|| {
|
||||||
datas
|
datas
|
||||||
.par_iter()
|
.par_iter()
|
||||||
@@ -201,7 +210,7 @@ fn preprocess_images<'py>(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
let (h, w, v, hash) = r.map_err(PyValueError::new_err)?;
|
let (h, w, v, hash) = r.map_err(PyValueError::new_err)?;
|
||||||
Ok((h, w, v.into_pyarray_bound(py), hash))
|
Ok((h, w, v.into_pyarray(py), hash))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -237,7 +246,6 @@ impl crate::registry::ImageProcessorSpec for InklingProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
#[pyo3(signature = (arr, raw_bytes, patch_size, rescale_frac=None, rescale_cap=None))]
|
#[pyo3(signature = (arr, raw_bytes, patch_size, rescale_frac=None, rescale_cap=None))]
|
||||||
fn rescale_patchify_hash<'py>(
|
fn rescale_patchify_hash<'py>(
|
||||||
@@ -261,22 +269,26 @@ fn rescale_patchify_hash<'py>(
|
|||||||
.as_slice()
|
.as_slice()
|
||||||
.map_err(|_| PyValueError::new_err("array must be C-contiguous"))?
|
.map_err(|_| PyValueError::new_err("array must be C-contiguous"))?
|
||||||
.to_vec();
|
.to_vec();
|
||||||
let (oh, ow, out) = py.allow_threads(move || {
|
let (oh, ow, out) = py.detach(move || {
|
||||||
common::pool().install(|| {
|
common::pool().install(|| {
|
||||||
let (tw, th) = common::resize::scaled_dims(w, h, rescale_frac, rescale_cap);
|
let (tw, th) = common::resize::scaled_dims(w, h, rescale_frac, rescale_cap);
|
||||||
let (rgb, h, w) = if (tw, th) != (w, h) {
|
let (rgb, h, w) = if (tw, th) != (w, h) {
|
||||||
(common::resize::resize_lanczos_rgb(&rgb, h, w, th, tw), th, tw)
|
(
|
||||||
|
common::resize::resize_lanczos_rgb(&rgb, h, w, th, tw),
|
||||||
|
th,
|
||||||
|
tw,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
(rgb, h, w)
|
(rgb, h, w)
|
||||||
};
|
};
|
||||||
(h, w, patchify_alloc(&rgb, h, w, patch_size))
|
(h, w, patchify_alloc(&rgb, h, w, patch_size))
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
Ok((oh, ow, out.into_pyarray_bound(py), hash))
|
Ok((oh, ow, out.into_pyarray(py), hash))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(parent: &Bound<'_, PyModule>) -> PyResult<()> {
|
pub fn register(parent: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||||
let m = PyModule::new_bound(parent.py(), "inkling")?;
|
let m = PyModule::new(parent.py(), "inkling")?;
|
||||||
m.add_function(wrap_pyfunction!(patchify_rgb, &m)?)?;
|
m.add_function(wrap_pyfunction!(patchify_rgb, &m)?)?;
|
||||||
m.add_function(wrap_pyfunction!(decode_patchify, &m)?)?;
|
m.add_function(wrap_pyfunction!(decode_patchify, &m)?)?;
|
||||||
m.add_function(wrap_pyfunction!(decode_patchify_batch, &m)?)?;
|
m.add_function(wrap_pyfunction!(decode_patchify_batch, &m)?)?;
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
//! Each model implements `ImageProcessorSpec` and registers itself. The Python
|
//! Each model implements `ImageProcessorSpec` and registers itself. The Python
|
||||||
//! layer looks up a processor by model name at init time.
|
//! layer looks up a processor by model name at init time.
|
||||||
|
|
||||||
use pyo3::prelude::*;
|
/// `(height, width, patches_as_u16_bits, content_hash)` for one image.
|
||||||
use pyo3::exceptions::PyValueError;
|
pub type PreprocessedImage = (usize, usize, Vec<u16>, u64);
|
||||||
|
|
||||||
/// Trait that each model's image processor must implement.
|
/// Trait that each model's image processor must implement.
|
||||||
pub trait ImageProcessorSpec: Send + Sync {
|
pub trait ImageProcessorSpec: Send + Sync {
|
||||||
@@ -12,15 +12,13 @@ pub trait ImageProcessorSpec: Send + Sync {
|
|||||||
fn name(&self) -> &'static str;
|
fn name(&self) -> &'static str;
|
||||||
|
|
||||||
/// Process a batch of raw image bytes: decode + preprocess + hash.
|
/// Process a batch of raw image bytes: decode + preprocess + hash.
|
||||||
///
|
|
||||||
/// Returns `(height, width, patches_as_u16_bits, content_hash)` per image.
|
|
||||||
fn preprocess_batch(
|
fn preprocess_batch(
|
||||||
&self,
|
&self,
|
||||||
datas: &[Vec<u8>],
|
datas: &[Vec<u8>],
|
||||||
patch_size: usize,
|
patch_size: usize,
|
||||||
rescale_frac: Option<f64>,
|
rescale_frac: Option<f64>,
|
||||||
rescale_cap: Option<i64>,
|
rescale_cap: Option<i64>,
|
||||||
) -> Result<Vec<(usize, usize, Vec<u16>, u64)>, String>;
|
) -> Result<Vec<PreprocessedImage>, String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Global registry of available processors.
|
/// Global registry of available processors.
|
||||||
@@ -28,6 +26,12 @@ pub struct ProcessorRegistry {
|
|||||||
specs: Vec<Box<dyn ImageProcessorSpec>>,
|
specs: Vec<Box<dyn ImageProcessorSpec>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for ProcessorRegistry {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ProcessorRegistry {
|
impl ProcessorRegistry {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { specs: Vec::new() }
|
Self { specs: Vec::new() }
|
||||||
@@ -38,7 +42,10 @@ impl ProcessorRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn lookup(&self, name: &str) -> Option<&dyn ImageProcessorSpec> {
|
pub fn lookup(&self, name: &str) -> Option<&dyn ImageProcessorSpec> {
|
||||||
self.specs.iter().find(|s| s.name() == name).map(|s| s.as_ref())
|
self.specs
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.name() == name)
|
||||||
|
.map(|s| s.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_names(&self) -> Vec<&'static str> {
|
pub fn list_names(&self) -> Vec<&'static str> {
|
||||||
|
|||||||
@@ -73,6 +73,15 @@ else
|
|||||||
find "$REPO_ROOT" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
find "$REPO_ROOT" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
|
||||||
rm -f "${REPO_ROOT}/python/pyproject.toml" && mv "${REPO_ROOT}/python/pyproject_other.toml" "${REPO_ROOT}/python/pyproject.toml"
|
rm -f "${REPO_ROOT}/python/pyproject.toml" && mv "${REPO_ROOT}/python/pyproject_other.toml" "${REPO_ROOT}/python/pyproject.toml"
|
||||||
|
|
||||||
|
# setuptools-rust builds the sglang-mm extension (sglang.srt.multimodal._core)
|
||||||
|
# declared in pyproject_other.toml, so a Rust toolchain must be present like
|
||||||
|
# on the CUDA/AMD CI paths. Idempotent; installs per-user under $HOME/.cargo.
|
||||||
|
# Export PATH here because the pip install below runs in this same shell
|
||||||
|
# (install_rustup.sh's own export/GITHUB_PATH only reach subsequent steps).
|
||||||
|
bash "${REPO_ROOT}/scripts/ci/utils/install_rustup.sh"
|
||||||
|
export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:${PATH}"
|
||||||
|
|
||||||
cd "${REPO_ROOT}" && ${PIP_INSTALL} -v -e "python[dev_musa]" --user
|
cd "${REPO_ROOT}" && ${PIP_INSTALL} -v -e "python[dev_musa]" --user
|
||||||
|
|
||||||
cd "${REPO_ROOT}/sgl-kernel"
|
cd "${REPO_ROOT}/sgl-kernel"
|
||||||
|
|||||||
@@ -2,8 +2,22 @@
|
|||||||
# Ensure a Rust toolchain (rustc/cargo) is installed for crates built from
|
# Ensure a Rust toolchain (rustc/cargo) is installed for crates built from
|
||||||
# source, e.g. the native gRPC extension bundled into the sglang wheel via
|
# source, e.g. the native gRPC extension bundled into the sglang wheel via
|
||||||
# setuptools-rust. Minimum supported version is 1.85 (edition 2024).
|
# setuptools-rust. Minimum supported version is 1.85 (edition 2024).
|
||||||
|
#
|
||||||
|
# Also pre-installs the workspace-pinned toolchain from rust/rust-toolchain.toml
|
||||||
|
# (best-effort) so cargo commands run inside rust/ don't pay the rustup
|
||||||
|
# auto-install on first use.
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
RUST_WORKSPACE_DIR="${SCRIPT_DIR}/../../../rust"
|
||||||
|
|
||||||
|
# Channel pinned by rust/rust-toolchain.toml, used as the default toolchain:
|
||||||
|
# setuptools-rust wheel builds run cargo from python/ — outside the pin's
|
||||||
|
# cwd-based scope — so only the default toolchain makes them use the same
|
||||||
|
# rustc as the workspace. Falls back to stable if the pin can't be parsed.
|
||||||
|
PINNED_CHANNEL="$(sed -n 's/^channel *= *"\([^"]*\)".*/\1/p' "${RUST_WORKSPACE_DIR}/rust-toolchain.toml" 2>/dev/null || true)"
|
||||||
|
DEFAULT_CHANNEL="${PINNED_CHANNEL:-stable}"
|
||||||
|
|
||||||
# Make cargo/rustc visible to the rest of this shell and to subsequent
|
# Make cargo/rustc visible to the rest of this shell and to subsequent
|
||||||
# GitHub Actions steps in the same job.
|
# GitHub Actions steps in the same job.
|
||||||
export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:${PATH}"
|
export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:${PATH}"
|
||||||
@@ -15,15 +29,68 @@ if [ -n "${GITHUB_PATH:-}" ]; then
|
|||||||
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> "${GITHUB_PATH}" || true
|
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> "${GITHUB_PATH}" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v cargo >/dev/null 2>&1 && command -v rustc >/dev/null 2>&1; then
|
# An in-cluster HTTP mirror may be available (e.g. on NPU runners); export it
|
||||||
echo "rust already installed: $(rustc --version), $(cargo --version)"
|
# up front so every rustup invocation below (self-heal and pinned-toolchain
|
||||||
exit 0
|
# install included) goes through the mirror.
|
||||||
|
if [ -n "${RUSTUP_CACHE_URL:-}" ]; then
|
||||||
|
export RUSTUP_DIST_SERVER="${RUSTUP_CACHE_URL}/rustup"
|
||||||
|
export RUSTUP_UPDATE_ROOT="${RUSTUP_CACHE_URL}/rustup/rustup"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "rust not found, installing via rustup..."
|
install_workspace_pinned_toolchain() {
|
||||||
|
# Pre-install the toolchain pinned by rust/rust-toolchain.toml: with no
|
||||||
|
# arguments and cwd inside rust/, `rustup toolchain install` (rustup >=
|
||||||
|
# 1.28) resolves channel/profile from the toolchain file; older rustups
|
||||||
|
# fall back to parsing the channel out of the file. Best-effort: the pin
|
||||||
|
# only governs cargo runs with cwd inside rust/ — setuptools-rust wheel
|
||||||
|
# builds run cargo from python/ and use the default toolchain, so a failure
|
||||||
|
# here must not fail the build (rustup auto-installs the pin on first use
|
||||||
|
# anyway).
|
||||||
|
if ! command -v rustup >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local toolchain_file="${RUST_WORKSPACE_DIR}/rust-toolchain.toml"
|
||||||
|
if [ ! -f "${toolchain_file}" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if (cd "${RUST_WORKSPACE_DIR}" && rustup toolchain install); then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local channel
|
||||||
|
channel="$(sed -n 's/^channel *= *"\([^"]*\)".*/\1/p' "${toolchain_file}")"
|
||||||
|
if [ -n "${channel}" ] && rustup toolchain install --profile minimal "${channel}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "WARNING: could not pre-install the toolchain pinned by ${toolchain_file}; rustup will auto-install it on first cargo use inside rust/"
|
||||||
|
}
|
||||||
|
|
||||||
# rustup.rs requires curl — make sure it's present.
|
if command -v cargo >/dev/null 2>&1 && command -v rustc >/dev/null 2>&1; then
|
||||||
if ! command -v curl >/dev/null 2>&1; then
|
# `command -v` only proves the rustup shims exist. A runner image can ship
|
||||||
|
# them with no default toolchain configured (rustup-init --default-toolchain
|
||||||
|
# none, or a removed toolchain), in which case every cargo/rustc call fails
|
||||||
|
# with "rustup could not choose a version of rustc to run". Check
|
||||||
|
# functionally and self-heal instead of trusting shim presence.
|
||||||
|
if rustc --version >/dev/null 2>&1; then
|
||||||
|
echo "rust already installed: $(rustc --version), $(cargo --version)"
|
||||||
|
elif command -v rustup >/dev/null 2>&1; then
|
||||||
|
echo "rustup shims present but no usable default toolchain; installing ${DEFAULT_CHANNEL} as default..."
|
||||||
|
# `rustup default <channel>` alone is not enough: the toolchain dir may
|
||||||
|
# exist but be corrupt (a partial install baked into a runner image
|
||||||
|
# fails later with "Missing manifest in toolchain '...'"), and rustup
|
||||||
|
# treats any existing dir as installed. Remove it and install fresh
|
||||||
|
# before selecting it as default.
|
||||||
|
rustup toolchain uninstall "${DEFAULT_CHANNEL}" || true
|
||||||
|
rustup toolchain install "${DEFAULT_CHANNEL}"
|
||||||
|
rustup default "${DEFAULT_CHANNEL}"
|
||||||
|
else
|
||||||
|
echo "ERROR: cargo/rustc on PATH but non-functional and rustup is missing; remove the stale binaries and re-run"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "rust not found, installing via rustup..."
|
||||||
|
|
||||||
|
# rustup.rs requires curl — make sure it's present.
|
||||||
|
if ! command -v curl >/dev/null 2>&1; then
|
||||||
if command -v apt-get &> /dev/null; then
|
if command -v apt-get &> /dev/null; then
|
||||||
apt-get update || true
|
apt-get update || true
|
||||||
apt-get install -y --no-install-recommends curl ca-certificates
|
apt-get install -y --no-install-recommends curl ca-certificates
|
||||||
@@ -33,12 +100,11 @@ if ! command -v curl >/dev/null 2>&1; then
|
|||||||
echo "ERROR: curl is required to install rustup, but no supported package manager was found"
|
echo "ERROR: curl is required to install rustup, but no supported package manager was found"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "${RUSTUP_CACHE_URL:-}" ]; then
|
if [ -n "${RUSTUP_CACHE_URL:-}" ]; then
|
||||||
# An in-cluster HTTP mirror is available (e.g. on NPU runners).
|
# Mirror env vars (RUSTUP_DIST_SERVER/RUSTUP_UPDATE_ROOT) were exported
|
||||||
export RUSTUP_DIST_SERVER="${RUSTUP_CACHE_URL}/rustup"
|
# at the top of this script.
|
||||||
export RUSTUP_UPDATE_ROOT="${RUSTUP_CACHE_URL}/rustup/rustup"
|
|
||||||
case "$(uname -m)" in
|
case "$(uname -m)" in
|
||||||
x86_64) RUSTUP_ARCH="x86_64-unknown-linux-gnu" ;;
|
x86_64) RUSTUP_ARCH="x86_64-unknown-linux-gnu" ;;
|
||||||
aarch64) RUSTUP_ARCH="aarch64-unknown-linux-gnu" ;;
|
aarch64) RUSTUP_ARCH="aarch64-unknown-linux-gnu" ;;
|
||||||
@@ -50,11 +116,14 @@ if [ -n "${RUSTUP_CACHE_URL:-}" ]; then
|
|||||||
"${RUSTUP_UPDATE_ROOT}/dist/${RUSTUP_ARCH}/rustup-init" \
|
"${RUSTUP_UPDATE_ROOT}/dist/${RUSTUP_ARCH}/rustup-init" \
|
||||||
-o "${RUSTUP_TMP}/rustup-init"
|
-o "${RUSTUP_TMP}/rustup-init"
|
||||||
chmod +x "${RUSTUP_TMP}/rustup-init"
|
chmod +x "${RUSTUP_TMP}/rustup-init"
|
||||||
"${RUSTUP_TMP}/rustup-init" -y --no-modify-path
|
"${RUSTUP_TMP}/rustup-init" -y --no-modify-path --default-toolchain "${DEFAULT_CHANNEL}"
|
||||||
else
|
else
|
||||||
curl --proto '=https' --tlsv1.2 --retry 3 --retry-delay 2 -sSf https://sh.rustup.rs \
|
curl --proto '=https' --tlsv1.2 --retry 3 --retry-delay 2 -sSf https://sh.rustup.rs \
|
||||||
| sh -s -- -y --no-modify-path
|
| sh -s -- -y --no-modify-path --default-toolchain "${DEFAULT_CHANNEL}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
install_workspace_pinned_toolchain
|
||||||
|
|
||||||
rustc --version
|
rustc --version
|
||||||
cargo --version
|
cargo --version
|
||||||
|
|||||||
Reference in New Issue
Block a user