Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b64d286
Qwen3-Omni native encoders + perf optimizations
t-avil Jun 30, 2026
508a7b4
Merge branch 'mstar-project:main' into encoders-implemeneted
t-avil Jun 30, 2026
b164ec4
qwen3-omni encoders: trim verbose comments
t-avil Jun 30, 2026
4c33b33
qwen3-omni encoders: fix ruff lint
t-avil Jun 30, 2026
7c76110
Merge main @78995a22 (piecewise CUDA graph runner made model-agnostic…
t-avil Jul 30, 2026
35b9940
qwen3-omni: run the encoders on PiecewiseCudaGraphRunner
t-avil Jul 30, 2026
d0dc6f8
Merge upstream/main into encoders-implemeneted
t-avil Jul 31, 2026
9b03ec9
qwen3-omni: remove _dump_obj tensor dumping
t-avil Aug 1, 2026
441c756
qwen3-omni: always use the vLLM prompt layout
t-avil Aug 1, 2026
e285f47
qwen3-omni: always run GPU log-mel and GPU image preprocessing
t-avil Aug 1, 2026
ec004a5
qwen3-omni: remove the unused audio-sentinel and batched-vision-prefi…
t-avil Aug 1, 2026
6488472
qwen3-omni: remove the native-encoder env override
t-avil Aug 1, 2026
534b05c
qwen3-omni: replace encoder capture-bucket and backend env vars with …
t-avil Aug 1, 2026
6893ffa
worker: send new-token counts instead of materialized token lists
t-avil Aug 1, 2026
ef23101
engine: make torch.compile dynamic=False opt-in
t-avil Aug 1, 2026
e107880
engine: fix the _compile_submodules docstring
t-avil Aug 1, 2026
73613c2
model: copy mutable non-tensor values in _clone_tensor_input
t-avil Aug 1, 2026
d47b76b
qwen3-omni: pass piecewise_runner as a forward argument
t-avil Aug 1, 2026
3783fcd
test: rewrite the encoder CUDA-graph parity tests
t-avil Aug 1, 2026
2da96cc
test: fix the GPU image-parity metrics
t-avil Aug 1, 2026
627a889
components: move varlen attention and encoder telemetry to model/comp…
t-avil Aug 1, 2026
d9ab6e5
qwen3-omni: rename _gpu_image_preprocess to _image_preprocess
t-avil Aug 1, 2026
0ac383a
qwen3-omni: run log-mel on the input's device instead of forcing cuda
t-avil Aug 1, 2026
3739202
qwen3-omni: keep decoded images as uint8 instead of float [0, 1]
t-avil Aug 1, 2026
407a6b4
qwen3-omni: run image preprocessing and log-mel on the GPU
t-avil Aug 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions benchmark/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,12 +485,14 @@ def __init__(
)
self.prompts = prompts

raw = load_dataset(
"ethz/food101",
split=split,
cache_dir=cache_dir,
trust_remote_code=True,
)
# ethz/food101 is now distributed as Parquet; newer `datasets` rejects
# `trust_remote_code`, so don't pass it (fall back only on older
# `datasets` that still require a loading script).
try:
raw = load_dataset("ethz/food101", split=split, cache_dir=cache_dir)
except (TypeError, ValueError):
raw = load_dataset("ethz/food101", split=split, cache_dir=cache_dir,
trust_remote_code=True)
# Shuffle with a fixed seed so we get class-diverse images while
# remaining deterministic across runs (so the same N images appear in
# the same order on every benchmark invocation).
Expand Down
18 changes: 18 additions & 0 deletions benchmark/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,17 @@ def _pct(values, p):
"output_bytes": dict(m.output_bytes),
})

import dataclasses

def _ls(x):
return dataclasses.asdict(x) if x is not None else None

def _lsmap(d):
return {k: _ls(v) for k, v in (d or {}).items()}

payload = {
"system": "ours",
"inference_system": self.config.inference_system.value,
"model": getattr(self.config.model, "__class__", type(self.config.model)).__name__,
"request_type": self.config.request_type.value,
"profiling_type": self.config.profiling_type.value,
Expand All @@ -273,6 +282,15 @@ def _pct(values, p):
"jct_p95_ms": _pct(jcts_ms, 95),
"jct_p99_ms": _pct(jcts_ms, 99),
"request_throughput": (agg.request_throughput or 0.0),
"text_token_throughput": agg.text_token_throughput,
"audio_seconds_throughput": agg.audio_seconds_throughput,
"audio_duration_mean_s": agg.audio_duration_mean_s,
"batch_size": agg.batch_size,
"max_concurrency": agg.max_concurrency,
"ttft": _lsmap(agg.ttft),
"itl": _lsmap(agg.itl),
"e2e_latency": _ls(agg.e2e_latency),
"rtf": _ls(agg.rtf),
"per_request": per_request,
}

Expand Down
7 changes: 7 additions & 0 deletions mstar/engine/cuda_graph_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,13 @@ class PiecewiseCudaGraphConfig(ABC):
# seq_lens for BATCHED, packed indptr for PACKED).
uses_kv_cache: bool = False
plan_fn: Callable[["BatchedCacheManager", PiecewiseCaptureShape], None] | None = None
# (5) attention planning without a KV cache, for regions running FlashInfer
# ragged varlen: the plan is host-side, so it cannot live in the graph.
# make_attn_state builds one wrapper per bucket (needs use_cuda_graph=True
# so replanning reuses the captured index buffers); plan_attn_fn replans it
# before each replay. Ignored when uses_kv_cache is True.
make_attn_state: Callable[[PiecewiseCaptureShape], Any] | None = None
plan_attn_fn: Callable[[Any, PiecewiseCaptureShape, list[int]], None] | None = None
# Whether the runner advances cache seq_lens (Python-only, post-replay) after
# each ``run``. True suits the common case where the captured region consumes
# its planned tokens once per step. Set False when the caller advances the
Expand Down
26 changes: 26 additions & 0 deletions mstar/engine/cuda_graph_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ class PiecewiseGraphData:
static_cache_manager: BatchedCacheManager | None
dummy_rids: list[str]
shape: PiecewiseCaptureShape
# Per-bucket no-KV attention wrapper, replanned outside the graph per replay.
# None for KV-cache configs, which plan through static_cache_manager.
attn_state: Any = None

@dataclass
class CudaGraphData:
Expand Down Expand Up @@ -2506,6 +2509,13 @@ def warmup_and_capture(self) -> None:
def _capture_one(self, shape: PiecewiseCaptureShape) -> None:
static_inputs = self.config.make_static_inputs(shape)
static_cm, dummy_rids = self._setup_cache_manager(shape)
# Built once so the graph captures its index buffers; replanned, never
# rebuilt, per replay.
attn_state = (
self.config.make_attn_state(shape)
if static_cm is None and self.config.make_attn_state is not None
else None
)

fn = self.config.capture_fn
if self.config.compile:
Expand All @@ -2517,15 +2527,21 @@ def _capture_one(self, shape: PiecewiseCaptureShape) -> None:
)

def run_fn():
extra = {} if attn_state is None else {"attn_state": attn_state}
return fn(
static_inputs=static_inputs,
static_cm=static_cm,
**extra,
**self.config.forward_kwargs,
)

def plan():
if static_cm is not None:
self._plan(static_cm, shape)
elif attn_state is not None and self.config.plan_attn_fn is not None:
# Bucket's own partition sums to total_tokens, so capture sees the
# widest indptr any replay can ask for.
self.config.plan_attn_fn(attn_state, shape, list(shape.seq_lens))

plan()

Expand Down Expand Up @@ -2557,6 +2573,7 @@ def plan():
static_cache_manager=static_cm,
dummy_rids=dummy_rids,
shape=shape,
attn_state=attn_state,
)

def _setup_cache_manager(
Expand Down Expand Up @@ -2787,6 +2804,15 @@ def run(
data.shape,
seq_lens=self._replay_seq_lens(data.shape, seq_lens, real_bs),
)
elif data.attn_state is not None and self.config.plan_attn_fn is not None:
# _replay_seq_lens zero-pads to shape.bs, so the indptr sums to the
# real token count and the pad tail lands in no segment; ragged
# attention is block-diagonal, so it cannot leak into real segments.
self.config.plan_attn_fn(
data.attn_state,
data.shape,
self._replay_seq_lens(data.shape, seq_lens, real_bs),
)

# --- 3: replay ---
data.graph.replay()
Expand Down
10 changes: 9 additions & 1 deletion mstar/engine/stateless_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,16 +543,24 @@ def _apply_torch_compile(self, node_name: str, submodule: NodeSubmodule) -> None
self.config.name, node_name,
)
return
# ``dynamic`` defaults to None (Inductor decides). A submodule whose
# forward only ever sees a fixed set of shapes -- e.g. one driven by
# piecewise capture buckets -- can set ``torch_compile_dynamic = False``
# to force static specialization. Leaving the default is important for
# variable-shape submodules, which would otherwise recompile per shape.
dynamic = getattr(submodule, "torch_compile_dynamic", None)
try:
if hasattr(submodule, "forward"):
submodule.forward = torch.compile(
submodule.forward,
fullgraph=False,
dynamic=dynamic,
)
logger.info(
"StatelessEngine[%s]: torch.compile applied to %s.forward",
"StatelessEngine[%s]: torch.compile applied to %s.forward (dynamic=%s)",
self.config.name,
node_name,
dynamic,
)
# forward_batched is intentionally left eager — Inductor would pay
# a ~30s one-shot trace cost for dynamic varlen shapes on the
Expand Down
50 changes: 50 additions & 0 deletions mstar/model/components/encoder_telemetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Piecewise-capture telemetry, shared by encoders.

A captured replay and a silent eager fallback produce identical results, so the
path taken is counted rather than inferred; ``encoder_path_counts()`` lets a
benchmark assert it measured the path it meant to.
"""
from __future__ import annotations

import logging

logger = logging.getLogger(__name__)

_ENCODER_PATH_COUNTS: dict[str, int] = {}
_SEEN_LAYOUTS: set[tuple] = set()
_SEEN_LAYOUTS_CAP = 512 # bounded: a long run must not leak keys
_WARNED_NO_BUCKET: set[str] = set()



def note_encoder_path(path: str) -> None:
_ENCODER_PATH_COUNTS[path] = _ENCODER_PATH_COUNTS.get(path, 0) + 1


def encoder_path_counts() -> dict[str, int]:
"""Snapshot of {path: count}, e.g. {"vision.piecewise": 96, "vision.eager": 0}."""
return dict(_ENCODER_PATH_COUNTS)


def note_encoder_layout(kind: str, n_seg: int, total_tokens: int, fitted: bool) -> None:
"""Log once per distinct layout; WARN the first time one fits no bucket —
the only visible signal that buckets are mis-sized, since the fallback is
otherwise silent."""
if not fitted and kind not in _WARNED_NO_BUCKET:
_WARNED_NO_BUCKET.add(kind)
logger.warning(
"%s encoder: NO capture bucket fits segments=%d total_tokens=%d — "
"falling back to eager. Widen CAPTURE_BATCH_SIZES_%s / "
"CAPTURE_TOKENS_%s; piecewise numbers from this run are "
"NOT measuring the captured path.",
kind, n_seg, total_tokens, kind.upper(), kind.upper(),
)
key = (kind, n_seg, total_tokens)
if key in _SEEN_LAYOUTS:
return
if len(_SEEN_LAYOUTS) < _SEEN_LAYOUTS_CAP:
_SEEN_LAYOUTS.add(key)
logger.info("%s encoder layout: segments=%d total_tokens=%d bucket=%s",
kind, n_seg, total_tokens, "HIT" if fitted else "MISS")


Loading
Loading