Add Inkling model support (#31681)

Co-authored-by: Chunan Zeng <zcnrex@gmail.com>
Co-authored-by: Ke Bao <ispobaoke@gmail.com>
Co-authored-by: Yanbin Jiang <jybsuper@gmail.com>
Co-authored-by: Yuhao Yang <47235274+yhyang201@users.noreply.github.com>
Co-authored-by: Qiaolin Yu <qiaolin.yu@radixark.ai>
Co-authored-by: Zhichen Zeng <zczeng@uw.edu>
Co-authored-by: Aurick Qiao <aurick@thinkingmachines.ai>
Co-authored-by: Joseph <jk@thinkingmachines.ai>
This commit is contained in:
Cheng Wan
2026-07-19 22:57:37 -07:00
committed by GitHub
co-authored by Chunan Zeng Ke Bao Yanbin Jiang Yuhao Yang Qiaolin Yu Zhichen Zeng Aurick Qiao Joseph
parent 829e9ce9d5
commit 02236fa38c
279 changed files with 74334 additions and 931 deletions
+1
View File
@@ -0,0 +1 @@
/target
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "sglang-mm"
version = "0.1.0"
edition = "2024"
description = "Rust-accelerated multimodal preprocessing for SGLang"
license = "Apache-2.0"
[lib]
name = "_core"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.23", features = ["extension-module"] }
numpy = "0.23"
rayon = "1.10"
half = "2.4"
image = { version = "0.25", default-features = false, features = ["jpeg", "png"] }
blake3 = "1"
base64 = "0.22"
[profile.release]
lto = true
codegen-units = 1
+120
View File
@@ -0,0 +1,120 @@
# sglang-mm
Rust-accelerated multimodal preprocessing for SGLang. Fused image decode,
resize, patchify, normalize, and content hash — all parallel and GIL-released.
Compiled as `sglang.srt.multimodal._core` via setuptools-rust when installing sglang.
## Architecture
```
src/
├── lib.rs # PyO3 module root (_core)
├── registry.rs # ImageProcessorSpec trait + ProcessorRegistry
├── common/
│ ├── mod.rs # thread pool, image decode, SHA256 hash, base64
│ ├── resize.rs # PIL-exact Lanczos resize
│ └── transforms.rs # reusable primitives: normalize, pad, extract_patches
└── <model>/
└── mod.rs # model-specific processor
```
## Python API
```python
from sglang.srt.multimodal._core import common, inkling
# Common (model-agnostic)
common.resize_rgb(arr, out_w, out_h)
common.scaled_dims(w, h, rescale_frac, rescale_cap)
common.image_decode_rgb(bytes) # -> (h, w, ndarray)
common.data_hash(bytes) # -> u64 SHA256
common.base64_decode(str) # -> bytes
# Model-specific
inkling.preprocess_images(list[bytes], ps, frac, cap) # -> [(h, w, bits, hash), ...]
inkling.decode_patchify(bytes, ps, frac, cap)
inkling.decode_patchify_batch(list[bytes], ps, frac, cap)
inkling.patchify_rgb(arr, patch_size)
```
## Adding a new model
1. Create `src/<model_name>/mod.rs`:
```rust
use crate::common;
use crate::registry::ImageProcessorSpec;
use rayon::prelude::*;
pub struct MyModelProcessor;
impl ImageProcessorSpec for MyModelProcessor {
fn name(&self) -> &'static str {
"my_model"
}
fn preprocess_batch(
&self,
datas: &[Vec<u8>],
patch_size: usize,
rescale_frac: Option<f64>,
rescale_cap: Option<i64>,
) -> Result<Vec<(usize, usize, Vec<u16>, u64)>, String> {
common::pool().install(|| {
datas.par_iter().map(|data| {
let hash = common::sha256_u64(data);
let (rgb, h, w) = common::decode_rescale(data, rescale_frac, rescale_cap)?;
// Use common::transforms::* or model-specific logic
let patches = my_patchify(&rgb, h, w, patch_size);
Ok((h, w, patches, hash))
}).collect()
})
}
}
```
2. Register in `src/registry.rs` `default_registry()`.
3. Add PyO3 bindings in `src/<model_name>/mod.rs` with a `register()` function.
4. Wire up in `src/lib.rs`: `mod my_model;` and `my_model::register(m)?;`.
5. Add Python processor class that calls `from sglang.srt.multimodal._core import my_model`.
## Available transform primitives (`common::transforms`)
| Function | Description |
|----------|-------------|
| `normalize_rgb_f32` | Single-pass `(pixel/255 - mean) / std` |
| `pad_to_grid` | Pad HWC image to grid-aligned dimensions |
| `extract_patches_hwc` | Reshape padded image into `[N, ph, pw, C]` patches |
| `patch_grid` | Compute `(nph, npw)` for given image and patch size |
## Design notes
- Thread pool capped at `min(8, cores)`. Override: `SGL_MM_RS_THREADS`.
- PNG decode is bit-exact vs PIL; JPEG may differ by ±1 LSB.
- Lanczos resize is a bit-exact clone of PIL's fixed-point implementation.
## Build
Automatically built when installing sglang:
```bash
pip install -e "python"
```
Or standalone for development:
```bash
cd rust/sglang-mm
pip install maturin
maturin develop --release
```
## Test
```bash
python bench/generate_golden.py # regenerate fixtures
pytest bench/test_golden.py # regression tests
python bench/bench_parity.py # parity + benchmark
```
+161
View File
@@ -0,0 +1,161 @@
import io
import time
import numpy as np
import torch
from PIL import Image
import sglang.srt.multimodal._core.inkling
from sglang.srt.multimodal.inkling.image_processing import (
IMAGE_MEAN,
IMAGE_STD,
PAD_NORM,
_encode_image_bytes,
_fill_patches_numba,
)
PS = 40
def ref_patchify(arr: np.ndarray) -> torch.Tensor:
h, w, _ = arr.shape
nph = (h + PS - 1) // PS
npw = w // PS + 1
patches = np.empty((nph * npw, PS, PS, 3), dtype=np.float32)
_fill_patches_numba(arr, PS, patches, IMAGE_MEAN, IMAGE_STD, PAD_NORM)
return torch.from_numpy(patches).to(torch.bfloat16)
def rs_patchify(arr: np.ndarray) -> torch.Tensor:
h, w, _ = arr.shape
nph = (h + PS - 1) // PS
npw = w // PS + 1
bits = sglang.srt.multimodal._core.inkling.patchify_rgb(arr, PS)
return torch.from_numpy(bits).view(torch.bfloat16).reshape(nph * npw, PS, PS, 3)
def rs_decode_patchify(data: bytes) -> torch.Tensor:
h, w, bits = sglang.srt.multimodal._core.inkling.decode_patchify(data, PS)
nph = (h + PS - 1) // PS
npw = w // PS + 1
return torch.from_numpy(bits).view(torch.bfloat16).reshape(nph * npw, PS, PS, 3)
def make_photo_like(h: int, w: int, seed: int = 0) -> np.ndarray:
rng = np.random.default_rng(seed)
yy, xx = np.mgrid[0:h, 0:w]
base = np.stack(
[
127 + 100 * np.sin(yy / 97.0) * np.cos(xx / 131.0),
127 + 100 * np.cos(yy / 61.0) * np.sin(xx / 89.0),
127 + 100 * np.sin((xx + yy) / 149.0),
],
axis=-1,
)
noise = rng.normal(0, 12, (h // 8 + 1, w // 8 + 1, 3))
noise = np.kron(noise, np.ones((8, 8, 1)))[:h, :w]
return np.clip(base + noise, 0, 255).astype(np.uint8)
def encode(arr: np.ndarray, fmt: str) -> bytes:
buf = io.BytesIO()
Image.fromarray(arr).save(
buf, format=fmt, **({"quality": 90} if fmt == "JPEG" else {})
)
return buf.getvalue()
def parity_a():
print("=== Parity A: patchify from decoded array (expect bit-exact) ===")
rng = np.random.default_rng(42)
for h, w in [(1080, 1920), (1920, 1080), (40, 40), (37, 53), (720, 1280), (1, 1)]:
arr = rng.integers(0, 256, (h, w, 3), dtype=np.uint8)
ref, got = ref_patchify(arr), rs_patchify(arr)
exact = torch.equal(ref.view(torch.uint16), got.view(torch.uint16))
print(f" {h}x{w}: shape {tuple(got.shape)} bit-exact={exact}")
assert exact, f"parity A failed at {h}x{w}"
def parity_b():
print("=== Parity B: full decode path ===")
arr = make_photo_like(1080, 1920)
for fmt in ["PNG", "JPEG"]:
data = encode(arr, fmt)
ref = _encode_image_bytes(
data,
patch_size=PS,
rescale_image_frac=None,
rescale_image_max_upscaled_long_edge=None,
)
got = rs_decode_patchify(data)
got2 = got.view(got.shape[0], 1, PS, PS, 3).expand(-1, 2, -1, -1, -1)
if torch.equal(
ref.contiguous().view(torch.uint16), got2.contiguous().view(torch.uint16)
):
print(f" {fmt}: bit-exact=True ({len(data)/1e6:.2f}MB)")
else:
d = (ref.float() - got2.float()).abs()
print(
f" {fmt}: bit-exact=False max_abs={d.max():.6f} mean_abs={d.mean():.8f} "
f"(decoder difference; normalized-feature units)"
)
def bench():
print("=== Benchmark (1080p, patch_size=40) ===")
arr = make_photo_like(1080, 1920)
jpeg = encode(arr, "JPEG")
n = 30
_encode_image_bytes(
jpeg,
patch_size=PS,
rescale_image_frac=None,
rescale_image_max_upscaled_long_edge=None,
)
rs_decode_patchify(jpeg)
sglang.srt.multimodal._core.inkling.decode_patchify_batch([jpeg] * 5, PS)
def run(label, fn, iters=n, images_per_call=1):
t0, c0 = time.perf_counter(), time.process_time()
for _ in range(iters):
fn()
wall = (time.perf_counter() - t0) / iters / images_per_call * 1e3
cpu = (time.process_time() - c0) / iters / images_per_call * 1e3
print(f" {label:42} wall {wall:8.2f} ms/img cpu {cpu:8.2f} ms/img")
return wall, cpu
w_py, c_py = run(
"python (PIL + numba + bf16 cast)",
lambda: _encode_image_bytes(
jpeg,
patch_size=PS,
rescale_image_frac=None,
rescale_image_max_upscaled_long_edge=None,
),
)
w_rs, c_rs = run("rust decode_patchify", lambda: rs_decode_patchify(jpeg))
w_rb, c_rb = run(
"rust decode_patchify_batch (5 imgs/call)",
lambda: sglang.srt.multimodal._core.inkling.decode_patchify_batch(
[jpeg] * 5, PS
),
iters=max(n // 5, 5),
images_per_call=5,
)
run("python numba patchify only", lambda: ref_patchify(arr))
run("rust patchify_rgb only", lambda: rs_patchify(arr))
print(
f"\n speedup vs python: single {w_py / w_rs:.1f}x wall / {c_py / c_rs:.1f}x cpu, "
f"batch {w_py / w_rb:.1f}x wall / {c_py / c_rb:.1f}x cpu"
)
if __name__ == "__main__":
torch.set_num_threads(8)
parity_a()
parity_b()
bench()
print("\nOK")
+12
View File
@@ -0,0 +1,12 @@
[build-system]
requires = ["maturin>=1.5,<2"]
build-backend = "maturin"
[project]
name = "sglang-mm"
version = "0.1.0"
description = "Rust-accelerated multimodal preprocessing for SGLang"
requires-python = ">=3.9"
[tool.maturin]
module-name = "_core"
+136
View File
@@ -0,0 +1,136 @@
pub mod resize;
pub mod transforms;
use std::sync::OnceLock;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray3, PyUntypedArrayMethods};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
pub fn pool() -> &'static rayon::ThreadPool {
static POOL: OnceLock<rayon::ThreadPool> = OnceLock::new();
POOL.get_or_init(|| {
let n = std::env::var("SGL_MM_RS_THREADS")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.filter(|&n| n > 0)
.unwrap_or_else(|| std::thread::available_parallelism().map_or(8, |c| c.get().min(8)));
rayon::ThreadPoolBuilder::new()
.num_threads(n)
.thread_name(|i| format!("sgl-mm-{i}"))
.build()
.expect("failed to build rayon pool")
})
}
pub fn sha256_u64(data: &[u8]) -> u64 {
let digest = blake3::hash(data);
u64::from_be_bytes(digest.as_bytes()[..8].try_into().unwrap())
}
pub fn decode_rgb(data: &[u8]) -> Result<(Vec<u8>, usize, usize), String> {
let img = image::load_from_memory(data).map_err(|e| format!("image decode: {e}"))?;
let rgb = img.to_rgb8();
let (w, h) = rgb.dimensions();
Ok((rgb.into_raw(), h as usize, w as usize))
}
pub fn decode_rescale(
data: &[u8],
rescale_frac: Option<f64>,
rescale_cap: Option<i64>,
) -> Result<(Vec<u8>, usize, usize), String> {
let (rgb, h, w) = decode_rgb(data)?;
let (tw, th) = resize::scaled_dims(w, h, rescale_frac, rescale_cap);
if (tw, th) == (w, h) {
return Ok((rgb, h, w));
}
Ok((resize::resize_lanczos_rgb(&rgb, h, w, th, tw), th, tw))
}
// --- Python-exposed functions ---
#[pyfunction]
pub fn resize_rgb<'py>(
py: Python<'py>,
arr: PyReadonlyArray3<'py, u8>,
out_w: usize,
out_h: usize,
) -> PyResult<Bound<'py, PyArray1<u8>>> {
if out_w == 0 || out_h == 0 {
return Err(PyValueError::new_err("output size must be positive"));
}
let shape = arr.shape();
let (h, w, c) = (shape[0], shape[1], shape[2]);
if c != 3 {
return Err(PyValueError::new_err(format!(
"expected HWC RGB array with 3 channels, got {c}"
)));
}
let data = arr
.as_slice()
.map_err(|_| PyValueError::new_err("array must be C-contiguous"))?
.to_vec();
let out = py.allow_threads(move || {
pool().install(|| resize::resize_lanczos_rgb(&data, h, w, out_h, out_w))
});
Ok(out.into_pyarray_bound(py))
}
#[pyfunction]
#[pyo3(signature = (w, h, rescale_frac=None, rescale_cap=None))]
pub fn scaled_dims(
w: usize,
h: usize,
rescale_frac: Option<f64>,
rescale_cap: Option<i64>,
) -> (usize, usize) {
resize::scaled_dims(w, h, rescale_frac, rescale_cap)
}
#[pyfunction]
pub fn image_decode_rgb<'py>(
py: Python<'py>,
data: Vec<u8>,
) -> PyResult<(usize, usize, Bound<'py, PyArray1<u8>>)> {
let (rgb, h, w) = py
.allow_threads(move || decode_rgb(&data))
.map_err(PyValueError::new_err)?;
Ok((h, w, rgb.into_pyarray_bound(py)))
}
#[pyfunction]
pub fn data_hash(py: Python<'_>, data: Vec<u8>) -> u64 {
py.allow_threads(move || {
let digest = blake3::hash(&data);
u64::from_be_bytes(digest.as_bytes()[..8].try_into().unwrap())
})
}
#[pyfunction]
pub fn base64_decode<'py>(
py: Python<'py>,
encoded: &str,
) -> PyResult<Bound<'py, pyo3::types::PyBytes>> {
use base64::Engine;
let decoded = py
.allow_threads(|| {
base64::engine::general_purpose::STANDARD
.decode(encoded)
.map_err(|e| format!("base64 decode error: {e}"))
})
.map_err(PyValueError::new_err)?;
Ok(pyo3::types::PyBytes::new_bound(py, &decoded))
}
pub fn register(parent: &Bound<'_, PyModule>) -> PyResult<()> {
let m = PyModule::new_bound(parent.py(), "common")?;
m.add_function(wrap_pyfunction!(resize_rgb, &m)?)?;
m.add_function(wrap_pyfunction!(scaled_dims, &m)?)?;
m.add_function(wrap_pyfunction!(image_decode_rgb, &m)?)?;
m.add_function(wrap_pyfunction!(data_hash, &m)?)?;
m.add_function(wrap_pyfunction!(base64_decode, &m)?)?;
parent.add_submodule(&m)?;
Ok(())
}
+185
View File
@@ -0,0 +1,185 @@
use rayon::prelude::*;
const PRECISION_BITS: i32 = 32 - 8 - 2;
fn sinc(x: f64) -> f64 {
if x == 0.0 {
return 1.0;
}
let x = x * std::f64::consts::PI;
x.sin() / x
}
fn lanczos(x: f64) -> f64 {
if (-3.0..3.0).contains(&x) {
sinc(x) * sinc(x / 3.0)
} else {
0.0
}
}
struct Coeffs {
bounds: Vec<(usize, usize)>,
kk: Vec<i32>,
ksize: usize,
}
fn precompute_coeffs(in_size: usize, out_size: usize) -> Coeffs {
let scale = in_size as f64 / out_size as f64;
let filterscale = if scale < 1.0 { 1.0 } else { scale };
let support = 3.0 * filterscale;
let ksize = support.ceil() as usize * 2 + 1;
let ss = 1.0 / filterscale;
let mut kkf = vec![0.0f64; out_size * ksize];
let mut bounds = vec![(0usize, 0usize); out_size];
for xx in 0..out_size {
let center = (xx as f64 + 0.5) * scale;
let mut xmin = (center - support + 0.5) as i32;
if xmin < 0 {
xmin = 0;
}
let mut xmax = (center + support + 0.5) as i32;
if xmax > in_size as i32 {
xmax = in_size as i32;
}
let count = (xmax - xmin) as usize;
let k = &mut kkf[xx * ksize..(xx + 1) * ksize];
let mut ww = 0.0f64;
for x in 0..count {
let w = lanczos((x as f64 + xmin as f64 - center + 0.5) * ss);
k[x] = w;
ww += w;
}
if ww != 0.0 {
for x in 0..count {
k[x] /= ww;
}
}
bounds[xx] = (xmin as usize, count);
}
let factor = (1i64 << PRECISION_BITS) as f64;
let kk = kkf
.iter()
.map(|&v| {
if v < 0.0 {
(-0.5 + v * factor) as i32
} else {
(0.5 + v * factor) as i32
}
})
.collect();
Coeffs { bounds, kk, ksize }
}
#[inline]
fn clip8(v: i32) -> u8 {
if v >= 1 << (PRECISION_BITS + 8) {
255
} else if v <= 0 {
0
} else {
(v >> PRECISION_BITS) as u8
}
}
fn resample_horizontal(src: &[u8], h: usize, w: usize, out_w: usize, c: &Coeffs) -> Vec<u8> {
let mut out = vec![0u8; h * out_w * 3];
out.par_chunks_mut(out_w * 3)
.enumerate()
.for_each(|(y, row)| {
let src_row = &src[y * w * 3..(y + 1) * w * 3];
for xx in 0..out_w {
let (xmin, count) = c.bounds[xx];
let k = &c.kk[xx * c.ksize..xx * c.ksize + count];
let mut s = [1i32 << (PRECISION_BITS - 1); 3];
for (x, &coef) in k.iter().enumerate() {
let p = (xmin + x) * 3;
s[0] += src_row[p] as i32 * coef;
s[1] += src_row[p + 1] as i32 * coef;
s[2] += src_row[p + 2] as i32 * coef;
}
let o = xx * 3;
row[o] = clip8(s[0]);
row[o + 1] = clip8(s[1]);
row[o + 2] = clip8(s[2]);
}
});
out
}
fn resample_vertical(src: &[u8], w: usize, out_h: usize, c: &Coeffs) -> Vec<u8> {
let mut out = vec![0u8; out_h * w * 3];
out.par_chunks_mut(w * 3)
.enumerate()
.for_each(|(yy, row)| {
let (ymin, count) = c.bounds[yy];
let k = &c.kk[yy * c.ksize..yy * c.ksize + count];
for x in 0..w {
let mut s = [1i32 << (PRECISION_BITS - 1); 3];
for (y, &coef) in k.iter().enumerate() {
let p = ((ymin + y) * w + x) * 3;
s[0] += src[p] as i32 * coef;
s[1] += src[p + 1] as i32 * coef;
s[2] += src[p + 2] as i32 * coef;
}
let o = x * 3;
row[o] = clip8(s[0]);
row[o + 1] = clip8(s[1]);
row[o + 2] = clip8(s[2]);
}
});
out
}
pub fn resize_lanczos_rgb(
src: &[u8],
h: usize,
w: usize,
out_h: usize,
out_w: usize,
) -> Vec<u8> {
let need_h = out_w != w;
let need_v = out_h != h;
if need_h && need_v {
let ch = precompute_coeffs(w, out_w);
let tmp = resample_horizontal(src, h, w, out_w, &ch);
let cv = precompute_coeffs(h, out_h);
resample_vertical(&tmp, out_w, out_h, &cv)
} else if need_h {
let ch = precompute_coeffs(w, out_w);
resample_horizontal(src, h, w, out_w, &ch)
} else if need_v {
let cv = precompute_coeffs(h, out_h);
resample_vertical(src, w, out_h, &cv)
} else {
src.to_vec()
}
}
pub fn scaled_dims(
w: usize,
h: usize,
frac: Option<f64>,
cap: Option<i64>,
) -> (usize, usize) {
let Some(frac) = frac else {
return (w, h);
};
let long_edge = w.max(h);
if long_edge == 0 {
return (w, h);
}
let mut target = long_edge as f64 * frac;
if let Some(cap) = cap {
let effective_cap = cap.max(long_edge as i64);
target = target.min(effective_cap as f64);
}
let ratio = target / long_edge as f64;
if ratio == 1.0 {
return (w, h);
}
let scale = |v: usize| ((v as f64 * ratio + 0.5).floor() as i64).max(1) as usize;
(scale(w), scale(h))
}
+93
View File
@@ -0,0 +1,93 @@
//! Reusable image transform primitives.
//!
//! Model-specific processors compose these to build their preprocessing
//! pipelines. All functions operate on flat RGB byte arrays (HWC layout).
/// Normalize u8 RGB pixels to f32 in a single pass: `(pixel/255 - mean) / std`.
///
/// Writes into `out` which must have length `h * w * 3`.
pub fn normalize_rgb_f32(
rgb: &[u8],
h: usize,
w: usize,
mean: &[f32; 3],
std: &[f32; 3],
out: &mut [f32],
) {
debug_assert_eq!(rgb.len(), h * w * 3);
debug_assert_eq!(out.len(), h * w * 3);
let inv255 = 1.0f32 / 255.0;
for i in 0..h * w {
for c in 0..3 {
let raw = rgb[i * 3 + c] as f32 * inv255;
out[i * 3 + c] = (raw - mean[c]) / std[c];
}
}
}
/// Pad an HWC image to a grid-aligned size, filling padded pixels with `pad_value`.
///
/// Returns the padded buffer and the new (height, width).
pub fn pad_to_grid(
rgb_f32: &[f32],
h: usize,
w: usize,
channels: usize,
grid_h: usize,
grid_w: usize,
pad_value: &[f32],
) -> (Vec<f32>, usize, usize) {
let new_h = ((h + grid_h - 1) / grid_h) * grid_h;
let new_w = ((w + grid_w - 1) / grid_w) * grid_w;
let mut out = vec![0.0f32; new_h * new_w * channels];
// Fill with pad value
for i in 0..new_h * new_w {
for c in 0..channels {
out[i * channels + c] = pad_value[c];
}
}
// Copy original data
for y in 0..h {
let src_start = y * w * channels;
let dst_start = y * new_w * channels;
out[dst_start..dst_start + w * channels]
.copy_from_slice(&rgb_f32[src_start..src_start + w * channels]);
}
(out, new_h, new_w)
}
/// Reshape a padded HWC image into patches of shape `[num_patches, ph, pw, C]`.
///
/// `h` and `w` must be divisible by `ph` and `pw` respectively.
pub fn extract_patches_hwc(
data: &[f32],
h: usize,
w: usize,
channels: usize,
ph: usize,
pw: usize,
) -> Vec<f32> {
let nph = h / ph;
let npw = w / pw;
let patch_size = ph * pw * channels;
let mut out = vec![0.0f32; nph * npw * patch_size];
for i in 0..nph {
for j in 0..npw {
let patch_idx = i * npw + j;
for y in 0..ph {
let src_y = i * ph + y;
let src_start = (src_y * w + j * pw) * channels;
let dst_start = patch_idx * patch_size + y * pw * channels;
out[dst_start..dst_start + pw * channels]
.copy_from_slice(&data[src_start..src_start + pw * channels]);
}
}
}
out
}
/// Compute the patch grid dimensions for a given image size and patch size.
#[inline]
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)
}
+287
View File
@@ -0,0 +1,287 @@
use std::sync::OnceLock;
use half::bf16;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray3, PyUntypedArrayMethods};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use rayon::prelude::*;
use crate::common;
const MEAN: [f32; 3] = [
0.48145466f64 as f32,
0.4578275f64 as f32,
0.40821073f64 as f32,
];
const STD: [f32; 3] = [
0.26862954f64 as f32,
0.2613026f64 as f32,
0.2757771f64 as f32,
];
const INV255: f32 = (1.0f64 / 255.0f64) as f32;
const PAD_RAW: f32 = (-1.0f64 / 255.0f64) as f32;
#[inline]
fn pad_bits() -> [u16; 3] {
core::array::from_fn(|c| bf16::from_f32((PAD_RAW - MEAN[c]) / STD[c]).to_bits())
}
fn luts() -> &'static [[u16; 256]; 3] {
static LUTS: OnceLock<[[u16; 256]; 3]> = OnceLock::new();
LUTS.get_or_init(|| {
core::array::from_fn(|c| {
core::array::from_fn(|v| {
let raw = v as u8 as f32 * INV255;
bf16::from_f32((raw - MEAN[c]) / STD[c]).to_bits()
})
})
})
}
#[inline]
pub fn grid(h: usize, w: usize, ps: usize) -> (usize, usize) {
((h + ps - 1) / ps, w / ps + 1)
}
fn patchify_into(arr: &[u8], h: usize, w: usize, ps: usize, out: &mut [u16]) {
let (_nph, npw) = grid(h, w, ps);
let pad = pad_bits();
let lut = luts();
let patch_elems = ps * ps * 3;
let row_elems = npw * patch_elems;
let body = |(i, row): (usize, &mut [u16])| {
let y_base = i * ps;
for j in 0..npw {
let x_base = j * ps;
let chunk = &mut row[j * patch_elems..(j + 1) * patch_elems];
for y in 0..ps {
let iy = y_base + y;
if iy >= h {
for x in 0..ps {
let o = (y * ps + x) * 3;
chunk[o..o + 3].copy_from_slice(&pad);
}
continue;
}
let n_real = if x_base < w { (w - x_base).min(ps) } else { 0 };
let src = (iy * w + x_base) * 3;
for x in 0..n_real {
let o = (y * ps + x) * 3;
let p = src + x * 3;
chunk[o] = lut[0][arr[p] as usize];
chunk[o + 1] = lut[1][arr[p + 1] as usize];
chunk[o + 2] = lut[2][arr[p + 2] as usize];
}
for x in n_real..ps {
let o = (y * ps + x) * 3;
chunk[o..o + 3].copy_from_slice(&pad);
}
}
}
};
common::pool().install(|| {
out.par_chunks_mut(row_elems).enumerate().for_each(body);
});
}
fn patchify_alloc(arr: &[u8], h: usize, w: usize, ps: usize) -> Vec<u16> {
let (nph, npw) = grid(h, w, ps);
let mut out = vec![0u16; nph * npw * ps * ps * 3];
patchify_into(arr, h, w, ps, &mut out);
out
}
fn check_ps(ps: usize) -> PyResult<()> {
if ps == 0 {
return Err(PyValueError::new_err("patch_size must be greater than zero"));
}
Ok(())
}
#[pyfunction]
fn patchify_rgb<'py>(
py: Python<'py>,
arr: PyReadonlyArray3<'py, u8>,
patch_size: usize,
) -> PyResult<Bound<'py, PyArray1<u16>>> {
check_ps(patch_size)?;
let shape = arr.shape();
let (h, w, c) = (shape[0], shape[1], shape[2]);
if c != 3 {
return Err(PyValueError::new_err(format!(
"expected HWC RGB array with 3 channels, got {c}"
)));
}
let data = arr
.as_slice()
.map_err(|_| PyValueError::new_err("array must be C-contiguous"))?
.to_vec();
let out = py.allow_threads(move || patchify_alloc(&data, h, w, patch_size));
Ok(out.into_pyarray_bound(py))
}
#[pyfunction]
#[pyo3(signature = (data, patch_size, rescale_frac=None, rescale_cap=None))]
fn decode_patchify<'py>(
py: Python<'py>,
data: Vec<u8>,
patch_size: usize,
rescale_frac: Option<f64>,
rescale_cap: Option<i64>,
) -> PyResult<(usize, usize, Bound<'py, PyArray1<u16>>)> {
check_ps(patch_size)?;
let (h, w, out) = py
.allow_threads(move || {
common::pool().install(|| {
let (rgb, h, w) = common::decode_rescale(&data, rescale_frac, rescale_cap)?;
Ok::<_, String>((h, w, patchify_alloc(&rgb, h, w, patch_size)))
})
})
.map_err(PyValueError::new_err)?;
Ok((h, w, out.into_pyarray_bound(py)))
}
#[pyfunction]
#[pyo3(signature = (datas, patch_size, rescale_frac=None, rescale_cap=None))]
fn decode_patchify_batch<'py>(
py: Python<'py>,
datas: Vec<Vec<u8>>,
patch_size: usize,
rescale_frac: Option<f64>,
rescale_cap: Option<i64>,
) -> PyResult<Vec<(usize, usize, Bound<'py, PyArray1<u16>>)>> {
check_ps(patch_size)?;
let results: Vec<Result<(usize, usize, Vec<u16>), String>> =
py.allow_threads(move || {
common::pool().install(|| {
datas
.par_iter()
.map(|data| {
let (rgb, h, w) = common::decode_rescale(data, rescale_frac, rescale_cap)?;
Ok((h, w, patchify_alloc(&rgb, h, w, patch_size)))
})
.collect()
})
});
results
.into_iter()
.map(|r| {
let (h, w, v) = r.map_err(PyValueError::new_err)?;
Ok((h, w, v.into_pyarray_bound(py)))
})
.collect()
}
#[pyfunction]
#[pyo3(signature = (datas, patch_size, rescale_frac=None, rescale_cap=None))]
fn preprocess_images<'py>(
py: Python<'py>,
datas: Vec<Vec<u8>>,
patch_size: usize,
rescale_frac: Option<f64>,
rescale_cap: Option<i64>,
) -> PyResult<Vec<(usize, usize, Bound<'py, PyArray1<u16>>, u64)>> {
check_ps(patch_size)?;
let results: Vec<Result<(usize, usize, Vec<u16>, u64), String>> =
py.allow_threads(move || {
common::pool().install(|| {
datas
.par_iter()
.map(|data| {
let hash = common::sha256_u64(data);
let (rgb, h, w) = common::decode_rescale(data, rescale_frac, rescale_cap)?;
Ok((h, w, patchify_alloc(&rgb, h, w, patch_size), hash))
})
.collect()
})
});
results
.into_iter()
.map(|r| {
let (h, w, v, hash) = r.map_err(PyValueError::new_err)?;
Ok((h, w, v.into_pyarray_bound(py), hash))
})
.collect()
}
/// Struct implementing ImageProcessorSpec for Inkling.
pub struct InklingProcessor;
impl crate::registry::ImageProcessorSpec for InklingProcessor {
fn name(&self) -> &'static str {
"inkling"
}
fn preprocess_batch(
&self,
datas: &[Vec<u8>],
patch_size: usize,
rescale_frac: Option<f64>,
rescale_cap: Option<i64>,
) -> Result<Vec<(usize, usize, Vec<u16>, u64)>, String> {
if patch_size == 0 {
return Err("patch_size must be greater than zero".into());
}
common::pool().install(|| {
datas
.par_iter()
.map(|data| {
let hash = common::sha256_u64(data);
let (rgb, h, w) = common::decode_rescale(data, rescale_frac, rescale_cap)?;
Ok((h, w, patchify_alloc(&rgb, h, w, patch_size), hash))
})
.collect()
})
}
}
#[pyfunction]
#[pyo3(signature = (arr, raw_bytes, patch_size, rescale_frac=None, rescale_cap=None))]
fn rescale_patchify_hash<'py>(
py: Python<'py>,
arr: PyReadonlyArray3<'py, u8>,
raw_bytes: &[u8],
patch_size: usize,
rescale_frac: Option<f64>,
rescale_cap: Option<i64>,
) -> PyResult<(usize, usize, Bound<'py, PyArray1<u16>>, u64)> {
check_ps(patch_size)?;
let shape = arr.shape();
let (h, w, c) = (shape[0], shape[1], shape[2]);
if c != 3 {
return Err(PyValueError::new_err(format!(
"expected HWC RGB array with 3 channels, got {c}"
)));
}
let hash = common::sha256_u64(raw_bytes);
let rgb = arr
.as_slice()
.map_err(|_| PyValueError::new_err("array must be C-contiguous"))?
.to_vec();
let (oh, ow, out) = py.allow_threads(move || {
common::pool().install(|| {
let (tw, th) = common::resize::scaled_dims(w, h, rescale_frac, rescale_cap);
let (rgb, h, w) = if (tw, th) != (w, h) {
(common::resize::resize_lanczos_rgb(&rgb, h, w, th, tw), th, tw)
} else {
(rgb, h, w)
};
(h, w, patchify_alloc(&rgb, h, w, patch_size))
})
});
Ok((oh, ow, out.into_pyarray_bound(py), hash))
}
pub fn register(parent: &Bound<'_, PyModule>) -> PyResult<()> {
let m = PyModule::new_bound(parent.py(), "inkling")?;
m.add_function(wrap_pyfunction!(patchify_rgb, &m)?)?;
m.add_function(wrap_pyfunction!(decode_patchify, &m)?)?;
m.add_function(wrap_pyfunction!(decode_patchify_batch, &m)?)?;
m.add_function(wrap_pyfunction!(preprocess_images, &m)?)?;
m.add_function(wrap_pyfunction!(rescale_patchify_hash, &m)?)?;
parent.add_submodule(&m)?;
Ok(())
}
+12
View File
@@ -0,0 +1,12 @@
mod common;
mod inkling;
pub mod registry;
use pyo3::prelude::*;
#[pymodule]
fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
common::register(m)?;
inkling::register(m)?;
Ok(())
}
+54
View File
@@ -0,0 +1,54 @@
//! Model processor registry.
//!
//! Each model implements `ImageProcessorSpec` and registers itself. The Python
//! layer looks up a processor by model name at init time.
use pyo3::prelude::*;
use pyo3::exceptions::PyValueError;
/// Trait that each model's image processor must implement.
pub trait ImageProcessorSpec: Send + Sync {
/// Short identifier, e.g. "inkling".
fn name(&self) -> &'static str;
/// Process a batch of raw image bytes: decode + preprocess + hash.
///
/// Returns `(height, width, patches_as_u16_bits, content_hash)` per image.
fn preprocess_batch(
&self,
datas: &[Vec<u8>],
patch_size: usize,
rescale_frac: Option<f64>,
rescale_cap: Option<i64>,
) -> Result<Vec<(usize, usize, Vec<u16>, u64)>, String>;
}
/// Global registry of available processors.
pub struct ProcessorRegistry {
specs: Vec<Box<dyn ImageProcessorSpec>>,
}
impl ProcessorRegistry {
pub fn new() -> Self {
Self { specs: Vec::new() }
}
pub fn register(&mut self, spec: Box<dyn ImageProcessorSpec>) {
self.specs.push(spec);
}
pub fn lookup(&self, name: &str) -> Option<&dyn ImageProcessorSpec> {
self.specs.iter().find(|s| s.name() == name).map(|s| s.as_ref())
}
pub fn list_names(&self) -> Vec<&'static str> {
self.specs.iter().map(|s| s.name()).collect()
}
}
/// Build the default registry with all compiled-in processors.
pub fn default_registry() -> ProcessorRegistry {
let mut reg = ProcessorRegistry::new();
reg.register(Box::new(crate::inkling::InklingProcessor));
reg
}
+40
View File
@@ -0,0 +1,40 @@
import io
import os
import sys
import numpy as np
import torch
from PIL import Image
sys.path.insert(0, os.path.dirname(__file__))
from bench_parity import PS, make_photo_like, ref_patchify
OUT = (
sys.argv[1]
if len(sys.argv) > 1
else os.path.join(os.path.dirname(__file__), "..", "tests", "golden")
)
CASES = [
("480x640", 480, 640, 10),
("200x320", 200, 320, 11),
("37x53", 37, 53, 12),
("40x40", 40, 40, 13),
]
os.makedirs(OUT, exist_ok=True)
for name, h, w, seed in CASES:
arr = make_photo_like(h, w, seed=seed)
bits = ref_patchify(arr).view(torch.uint16).numpy()
buf = io.BytesIO()
Image.fromarray(arr).save(buf, format="PNG")
path = os.path.join(OUT, f"golden_{name}.npz")
np.savez_compressed(
path,
arr=arr,
bits=bits,
png=np.frombuffer(buf.getvalue(), dtype=np.uint8),
patch_size=np.int64(PS),
)
print(f" {path}: input {h}x{w}, bits {bits.shape}")
print("GOLDEN_OK")
+51
View File
@@ -0,0 +1,51 @@
import glob
import os
import numpy as np
import pytest
import sglang.srt.multimodal._core.inkling
GOLDEN_DIR = os.environ.get(
"INKLING_MM_GOLDEN_DIR",
os.path.join(os.path.dirname(__file__), "..", "tests", "golden"),
)
GOLDENS = sorted(glob.glob(os.path.join(GOLDEN_DIR, "golden_*.npz")))
def bf16_bits_to_f32(bits: np.ndarray) -> np.ndarray:
return (bits.astype(np.uint32) << 16).view(np.float32)
@pytest.mark.parametrize("path", GOLDENS, ids=[os.path.basename(p) for p in GOLDENS])
def test_patchify_rgb_bit_exact(path):
g = np.load(path)
got = sglang.srt.multimodal._core.inkling.patchify_rgb(
g["arr"], int(g["patch_size"])
)
np.testing.assert_array_equal(got, g["bits"].reshape(-1))
@pytest.mark.parametrize("path", GOLDENS, ids=[os.path.basename(p) for p in GOLDENS])
def test_decode_patchify_png_bit_exact(path):
g = np.load(path)
h_ref, w_ref = g["arr"].shape[:2]
h, w, got = sglang.srt.multimodal._core.inkling.decode_patchify(
g["png"].tobytes(), int(g["patch_size"])
)
assert (h, w) == (h_ref, w_ref)
np.testing.assert_array_equal(got, g["bits"].reshape(-1))
def test_batch_matches_single():
gs = [np.load(p) for p in GOLDENS]
data = [g["png"].tobytes() for g in gs]
ps = int(gs[0]["patch_size"])
for (h, w, bits), g in zip(
sglang.srt.multimodal._core.inkling.decode_patchify_batch(data, ps), gs
):
np.testing.assert_array_equal(bits, g["bits"].reshape(-1))
def test_golden_fixtures_exist():
assert len(GOLDENS) >= 4, f"expected golden fixtures in {GOLDEN_DIR}"
+103
View File
@@ -0,0 +1,103 @@
import asyncio
import base64
import io
import os
import sys
import time
from types import SimpleNamespace
import numpy as np
import soundfile as sf
import torch
from PIL import Image
sys.path.insert(0, os.path.dirname(__file__))
from bench_parity import make_photo_like
from sglang.srt.managers.mm_utils import data_hash, hash_feature
from sglang.srt.multimodal.inkling import InklingProcessor
from sglang.srt.multimodal.processors import inkling as prc
def png_bytes(arr):
buf = io.BytesIO()
Image.fromarray(arr).save(buf, format="PNG")
return buf.getvalue()
def wav_bytes(seconds=1.0, sr=16000):
t = np.linspace(0, seconds, int(sr * seconds), endpoint=False)
buf = io.BytesIO()
sf.write(
buf, (0.3 * np.sin(2 * np.pi * 440 * t)).astype(np.float32), sr, format="WAV"
)
return buf.getvalue()
def make_proc():
proc = prc.InklingMultimodalProcessor.__new__(prc.InklingMultimodalProcessor)
proc.IMAGE_TOKEN_ID = 100
proc.AUDIO_TOKEN_ID = 101
proc.AUDIO_END_TOKEN_ID = 102
proc.inkling_processor = InklingProcessor()
return proc
proc = make_proc()
img = png_bytes(make_photo_like(200, 320, seed=7))
aud = wav_bytes()
out = proc.assemble([1, 100, 2, 101, 3], [img], [aud])
img_item = next(i for i in out.mm_items if i.modality.name == "IMAGE")
aud_item = next(i for i in out.mm_items if i.modality.name == "AUDIO")
assert img_item.hash == data_hash(img), "image hash != data_hash(raw bytes)"
assert aud_item.hash == data_hash(aud), "audio hash != data_hash(raw bytes)"
print(f" assemble: image hash={img_item.hash:#x} audio hash={aud_item.hash:#x} OK")
h0 = img_item.hash
img_item.set_pad_value()
assert img_item.hash == h0 and img_item.pad_value is not None
print(f" set_pad_value: hash preserved, pad_value={img_item.pad_value} OK")
out2 = proc.assemble([1, 100, 2], [img], [])
assert out2.mm_items[0].hash == h0
print(" determinism: same bytes -> same hash OK")
data_url = "data:image/png;base64," + base64.b64encode(img).decode()
req = SimpleNamespace(input_ids=[1, 100, 100, 2])
out3 = asyncio.run(
proc.process_mm_data_async(
image_data=[data_url, data_url], audio_data=None, request_obj=req
)
)
assert all(i.hash == h0 for i in out3.mm_items), "data: URL roundtrip hash mismatch"
print(" process_mm_data_async: concurrent resolve + hash OK")
orig = prc._resolve_media_item
prc._resolve_media_item = lambda it: (time.sleep(0.3), orig(it))[1]
t0 = time.perf_counter()
asyncio.run(prc._resolve_media_items([data_url] * 8))
elapsed = time.perf_counter() - t0
prc._resolve_media_item = orig
assert elapsed < 1.2, f"8x 0.3s resolves took {elapsed:.2f}s; expected ~0.3s"
print(f" concurrency: 8 x 0.3s resolves in {elapsed:.2f}s OK")
imgs_5 = [png_bytes(make_photo_like(1080, 1920, seed=s)) for s in range(5)]
feats = [
torch.randn(1323, 1, 40, 40, 3, dtype=torch.bfloat16).expand(1323, 2, 40, 40, 3)
for _ in range(5)
]
t0 = time.perf_counter()
for b in imgs_5:
data_hash(b)
t_bytes = (time.perf_counter() - t0) * 1e3
t0 = time.perf_counter()
for f in feats:
hash_feature(f)
t_feat = (time.perf_counter() - t0) * 1e3
print(
f" hash cost 5 imgs: raw bytes {t_bytes:.1f}ms vs feature tensor {t_feat:.1f}ms "
f"({t_feat / t_bytes:.0f}x)"
)
print("HASH_FETCH_OK")
+86
View File
@@ -0,0 +1,86 @@
import io
import os
import sys
import numpy as np
import torch
from PIL import Image
sys.path.insert(0, os.path.dirname(__file__))
from bench_parity import make_photo_like
import sglang.srt.multimodal.inkling.image_processing as ip
def encode(arr, fmt):
buf = io.BytesIO()
Image.fromarray(arr).save(
buf, format=fmt, **({"quality": 90} if fmt == "JPEG" else {})
)
return buf.getvalue()
def run(images, use_rs: bool, rescale: bool):
ip._rs_module = None
os.environ["SGLANG_RS_MM_PREPROCESS"] = "1" if use_rs else "0"
kwargs = (
{}
if rescale
else {"rescale_image_frac": None, "rescale_image_max_upscaled_long_edge": None}
)
proc = ip.InklingImageProcessor(patch_size=40, **kwargs)
out = proc.preprocess(images)
assert (ip._rs_module is not False) == use_rs, "rust module gating mismatch"
return out
def compare(tag, images, expect_exact, rescale=False):
ref = run(images, use_rs=False, rescale=rescale)
got = run(images, use_rs=True, rescale=rescale)
assert ref["num_patches"] == got["num_patches"], tag
assert ref["num_tokens"] == got["num_tokens"], tag
a, b = ref["vision_patches_bthwc"], got["vision_patches_bthwc"]
assert a.shape == b.shape and a.dtype == b.dtype, f"{tag}: {a.shape} vs {b.shape}"
exact = torch.equal(
a.contiguous().view(torch.uint16), b.contiguous().view(torch.uint16)
)
if expect_exact:
assert exact, f"{tag}: expected bit-exact"
print(f" {tag}: bit-exact=True shape={tuple(a.shape)}")
else:
d = (a.float() - b.float()).abs()
print(
f" {tag}: bit-exact={exact} max_abs={d.max():.6f} shape={tuple(a.shape)}"
)
assert d.max() < 0.25, f"{tag}: JPEG decoder diff too large"
arr1 = make_photo_like(1080, 1920, seed=1)
arr2 = make_photo_like(720, 1280, seed=2)
arr3 = make_photo_like(480, 640, seed=3)
print("=== integration: InklingImageProcessor env-gated rust path ===")
compare("single PNG", [encode(arr1, "PNG")], expect_exact=True)
compare("single JPEG", [encode(arr1, "JPEG")], expect_exact=False)
compare(
"5x PNG batch",
[encode(a, "PNG") for a in [arr1, arr2, arr3, arr1, arr2]],
expect_exact=True,
)
compare(
"mixed JPEG/PNG batch",
[encode(arr1, "JPEG"), encode(arr2, "PNG")],
expect_exact=False,
)
compare("PIL input (PNG roundtrip)", [Image.fromarray(arr3)], expect_exact=True)
compare("single PNG rescaled", [encode(arr1, "PNG")], expect_exact=True, rescale=True)
compare(
"single JPEG rescaled", [encode(arr1, "JPEG")], expect_exact=False, rescale=True
)
compare(
"3x mixed rescaled",
[encode(arr1, "JPEG"), encode(arr2, "PNG"), encode(arr3, "PNG")],
expect_exact=False,
rescale=True,
)
print("INTEGRATION_OK")
+121
View File
@@ -0,0 +1,121 @@
import math
import time
from typing import Optional
import numpy as np
import pytest
from PIL import Image
import sglang.srt.multimodal._core.inkling
def py_scaled_dims(
width: int,
height: int,
frac: Optional[float],
cap: Optional[int],
):
if frac is None:
return width, height
long_edge = max(width, height)
if long_edge == 0:
return width, height
target = float(long_edge) * frac
if cap is not None:
target = min(target, float(max(cap, long_edge)))
ratio = target / float(long_edge)
if ratio == 1.0:
return width, height
def scale(value):
return max(1, math.floor(float(value) * ratio + 0.5))
return scale(width), scale(height)
def pil_resize(arr: np.ndarray, tw: int, th: int) -> np.ndarray:
return np.array(
Image.fromarray(arr).resize((tw, th), resample=Image.Resampling.LANCZOS),
dtype=np.uint8,
)
def rs_resize(arr: np.ndarray, tw: int, th: int) -> np.ndarray:
return sglang.srt.multimodal._core.inkling.resize_rgb(arr, tw, th).reshape(
th, tw, 3
)
CASES = [
(1080, 1920, 1152, 2048),
(896, 896, 1792, 1792),
(360, 640, 720, 1280),
(37, 53, 74, 106),
(100, 100, 173, 173),
(1, 1, 2, 2),
(256, 256, 100, 100),
(720, 1280, 720, 1280),
(3, 500, 6, 1000),
]
@pytest.mark.parametrize(
"h,w,th,tw", CASES, ids=[f"{h}x{w}->{th}x{tw}" for h, w, th, tw in CASES]
)
def test_resize_bit_exact(h, w, th, tw):
rng = np.random.default_rng(h * 10000 + w)
arr = rng.integers(0, 256, (h, w, 3), dtype=np.uint8)
np.testing.assert_array_equal(rs_resize(arr, tw, th), pil_resize(arr, tw, th))
def test_scaled_dims_sweep():
rng = np.random.default_rng(0)
sizes = [(int(a), int(b)) for a, b in rng.integers(1, 5000, (500, 2))]
sizes += [(2048, 1024), (2049, 100), (1024, 2048), (1, 1), (4096, 4096)]
for frac, cap in [(2.0, 2048), (1.5, 2048), (3.0, None), (None, None), (2.0, 1)]:
for w, h in sizes:
assert sglang.srt.multimodal._core.inkling.scaled_dims(
w, h, frac, cap
) == py_scaled_dims(w, h, frac, cap), (
w,
h,
frac,
cap,
)
def test_decode_patchify_rescaled_matches_pil_pipeline():
import io
import torch
rng = np.random.default_rng(7)
arr = rng.integers(0, 256, (1080, 1920, 3), dtype=np.uint8)
buf = io.BytesIO()
Image.fromarray(arr).save(buf, format="PNG")
h, w, bits = sglang.srt.multimodal._core.inkling.decode_patchify(
buf.getvalue(), 40, 2.0, 2048
)
assert (w, h) == py_scaled_dims(1920, 1080, 2.0, 2048)
ref_arr = pil_resize(arr, w, h)
ref_bits = sglang.srt.multimodal._core.inkling.patchify_rgb(ref_arr, 40)
np.testing.assert_array_equal(bits, ref_bits)
assert torch.from_numpy(bits).view(torch.bfloat16).shape[0] > 0
def test_resize_bench():
arr = np.random.default_rng(1).integers(0, 256, (1080, 1920, 3), dtype=np.uint8)
tw, th = py_scaled_dims(1920, 1080, 2.0, 2048)
pil_resize(arr, tw, th)
rs_resize(arr, tw, th)
t0 = time.perf_counter()
for _ in range(10):
pil_resize(arr, tw, th)
t_pil = (time.perf_counter() - t0) / 10 * 1e3
t0 = time.perf_counter()
for _ in range(10):
rs_resize(arr, tw, th)
t_rs = (time.perf_counter() - t0) / 10 * 1e3
print(
f"\nresize 1920x1080->{tw}x{th}: PIL {t_pil:.1f}ms rust {t_rs:.1f}ms ({t_pil/t_rs:.1f}x)"
)