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:
co-authored by
Chunan Zeng
Ke Bao
Yanbin Jiang
Yuhao Yang
Qiaolin Yu
Zhichen Zeng
Aurick Qiao
Joseph
parent
829e9ce9d5
commit
02236fa38c
@@ -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")
|
||||
@@ -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}"
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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)"
|
||||
)
|
||||
Reference in New Issue
Block a user