Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
ecde471
[Model] Serve LoRA adapters on Qwen3.5, Orpheus, Higgs Audio, Qwen3-O…
dghosef Oct 22, 2026
8fbfa15
[Test] LoRA on each model's own stack against merged weights
dghosef Oct 22, 2026
90b4909
[Docs][Benchmark] LoRA models, and the GPU parity and competitor harn…
dghosef Oct 22, 2026
97880be
[Docs] LoRA: list Fish Audio S2-Pro among the models that take no ada…
dghosef Oct 22, 2026
3ade277
module_parity: find Qwen3-Omni's thinker text stack where transformer…
dghosef Oct 30, 2026
f0b67e4
module_parity: a transformers reference for the Qwen3-TTS Talker
dghosef Oct 30, 2026
7dd9ebb
module_parity: keep the hf references' RoPE frequencies in fp32
dghosef Oct 30, 2026
78916fd
module_parity: run the Omni reference on transformers 5 with or witho…
dghosef Oct 30, 2026
ccd3db3
Docs: restate the LoRA parity misses for Qwen3-Omni, Qwen3-TTS and Ch…
dghosef Oct 30, 2026
3e1e06b
[Test] LoRA models: follow main's code predictor, pool plan and admis…
dghosef Nov 2, 2026
6474cef
Qwen3.5: embed a vision prompt's ids inside the step (R1.F1, R1.F5)
dghosef Nov 3, 2026
2b19d6d
Qwen3.5: state checkpoint rules on both weights roots (R1.F7)
dghosef Nov 3, 2026
b175156
Qwen3.5: name the vision targets by default (R1.F9)
dghosef Nov 3, 2026
9550e13
Test the compiled conv patch embed with an adapter row (R1.F15)
dghosef Nov 3, 2026
e1b1e00
Load the LoRA models through their checkpoint rules only (R1.F8)
dghosef Nov 3, 2026
635ecc1
LoRA harnesses: follow main's renames and features (R1.F2, R1.F4, R1.…
dghosef Nov 3, 2026
3f5f8ae
Docs: LoRA paragraphs, the Orpheus example, the full not-yet list (R1…
dghosef Nov 3, 2026
531a2ec
TP read tests: embed a vision row in the step, as Qwen3.5 now does (R…
dghosef Nov 3, 2026
61292ce
LoRA: delete module_lora_spec(deferred=) and LoraBound.lora_deferred …
dghosef Nov 3, 2026
043c1b8
TP read tests: name and describe the in-step embedding collective (R2…
dghosef Nov 3, 2026
7be9c60
Qwen3.5: splice vision rows with index_copy_, not masked_scatter_ (R2…
dghosef Nov 3, 2026
aaa1639
Qwen3.5 vision: a leased step's frames plan its LoRA rows base-only (…
dghosef Nov 4, 2026
fb5f448
LoRA GPU harnesses: follow main's step order and prefix-key declarati…
dghosef Nov 4, 2026
6e0d46c
prefix_cache preflight: check for LoRA first, so a bare instance's kv…
dghosef Nov 4, 2026
33c895b
module_parity: merge the adapter into every checkpoint file that hold…
dghosef Nov 4, 2026
e9b561f
Merge origin/main into lora/3-models
dghosef Nov 5, 2026
7181593
Qwen3Attention: bind without a position resource, as ParallelAttentio…
dghosef Nov 5, 2026
bc0efe0
Qwen3 ViT: the patch embed's GEMM is its proj's own forward, so a LoR…
dghosef Nov 5, 2026
35f2da5
docs: mirror this branch's LoRA guide changes into the Mintlify pages
dghosef Nov 5, 2026
47b4e1c
test: the compiled patch-embed test's docstring describes PatchProjec…
dghosef Nov 5, 2026
c8e8423
docs: a lora block turns main's resident decode planning off on the L…
dghosef Nov 5, 2026
597b5d2
benchmark/lora README: base vs off now includes resident planning; th…
dghosef Nov 5, 2026
0c43e56
benchmark/lora README: what base vs off pays, as the phase tables sho…
dghosef Nov 5, 2026
35446a0
benchmark/lora README: read the planning gap off res.pre_plan.*
dghosef Nov 5, 2026
f21d59b
benchmark/lora load: count tokens from the replies, as usage is all z…
dghosef Nov 6, 2026
f074ae7
module_parity: serve Qwen3-TTS greedy on every codebook
dghosef Nov 6, 2026
18c0bee
Merge origin/main into lora/3-models
dghosef Nov 8, 2026
c08b75a
LoRA: serve an adapter on a fused FusedColumnLinear (Qwen3.5's ViT qkv)
dghosef Nov 8, 2026
ed42cdc
qwen3_5/lora.py: a vision prompt is embedded in the forward now
dghosef Nov 8, 2026
a1b1bc0
module_parity: Chatterbox's serve requests carry seed 0
dghosef Nov 8, 2026
d8e4f8e
test: a fused adapter with no shard id on an unequal FusedColumnLinear
dghosef Nov 8, 2026
8999c20
test: the unbound-forward check skips fused_column_whole, fused_colum…
dghosef Nov 8, 2026
ed91a47
Refuse LoRA on a node the async TP scheduler runs at tp_size > 1, unt…
dghosef Nov 8, 2026
54aa316
LoRA under TP: name the env var when it is the switch; the parity TP …
dghosef Nov 8, 2026
c1505e7
LoRA under TP: the env advice says set it to 0 or a node list; the pa…
dghosef Nov 8, 2026
265d0cc
Merge origin/main into lora/3-models
dghosef Nov 8, 2026
bec7ffa
validation: read the async TP setting from main's lockstep module
dghosef Nov 8, 2026
d92fd18
LoRA under TP: drop the async-scheduler refusal; main's teardown poin…
dghosef Nov 8, 2026
3b1ef57
LoRA: the pool refuses CFG parallelism beside SP
dghosef Nov 8, 2026
5608aa2
LoRA: the pool accepts only TP among the mesh axes, so a new axis is …
dghosef Nov 8, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ uv.lock
!examples/observability/grafana/dashboards/*.json
!mstar/utils/fused_moe/configs/*.json
!mstar/model/command_a_plus/tests/fixtures/tiny_config.json
!test/modular/fixtures/*.json
profiler/
traces/

Expand Down
313 changes: 308 additions & 5 deletions benchmark/lora/README.md

Large diffs are not rendered by default.

1,243 changes: 1,243 additions & 0 deletions benchmark/lora/compare_engines.py

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions benchmark/lora/eager_site/sitecustomize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Put this directory on ``PYTHONPATH`` and every Python process started with it (the
server, the conductor, each worker) serves Qwen3.5's LLM node eagerly: no captured
graphs, no ``torch.compile``. For the captured-vs-eager checks in ``benchmark/lora``
only; nothing in mstar reads it.

The patch is applied when ``mstar.model.qwen3_5.submodules`` is first imported, so a
process that never loads the model pays nothing. Check the server log for
``torch.compile disabled for LLM`` to confirm it took.
"""

import importlib.abc
import importlib.machinery
import sys

_TARGET = "mstar.model.qwen3_5.submodules"


def _eager(module) -> None:
cls = module.LLMSubmodule
cls.get_accelerator_graph_configs = lambda self, *args, **kwargs: []
cls.disable_torch_compile = True


class _PatchOnImport(importlib.abc.MetaPathFinder):
def find_spec(self, name, path, target=None):
if name != _TARGET:
return None
sys.meta_path.remove(self)
spec = importlib.machinery.PathFinder.find_spec(name, path)
if spec is None or spec.loader is None:
return spec
exec_module = spec.loader.exec_module

def patched(module):
exec_module(module)
_eager(module)

spec.loader.exec_module = patched
return spec


sys.meta_path.insert(0, _PatchOnImport())
105 changes: 105 additions & 0 deletions benchmark/lora/load.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""A closed-loop chat load whose requests name LoRA adapters, round-robin.

``benchmark.runner`` sends the base model's name in every request, so it cannot put
rows on adapters; this keeps ``--concurrency`` requests in flight, each naming the next
entry of ``--models`` (an adapter's name, or the base model's), with a fixed output
length (``ignore_eos``) so every request is the same work and the worker sits at one
batch size.

python -m benchmark.lora.load --url http://127.0.0.1:8100 --concurrency 32 \
--models qwen3_5_0.8b,proofreader,router,pinocchio --requests 256
"""

from __future__ import annotations

import argparse
import concurrent.futures
import itertools
import json
import threading
import time
import urllib.error
import urllib.request

PROMPTS = [
"Write a short story about a lighthouse keeper who finds a message in a bottle.",
"Explain how a hash map handles collisions, with an example.",
"Describe the water cycle to a ten-year-old.",
"List the steps to make bread from scratch, with timings.",
]


def _chat(url: str, model: str, prompt: str, max_tokens: int, timeout: float) -> int:
body = json.dumps({
"model": model, "messages": [{"role": "user", "content": prompt}],
"temperature": 0, "max_tokens": max_tokens, "ignore_eos": True,
"enable_thinking": False,
}).encode()
req = urllib.request.Request(
f"{url}/v1/chat/completions", data=body, headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=timeout) as r:
out = json.loads(r.read())
return int(out.get("usage", {}).get("completion_tokens", 0))


def closed_loop(
url: str, models: list[str], concurrency: int, requests: int, max_tokens: int,
warmup: int = 0, timeout: float = 600.0,
) -> dict:
"""``warmup`` requests first (not counted), then ``requests`` with ``concurrency``
in flight. Returns throughput and latency."""
order = itertools.cycle(models)
lock = threading.Lock()

def one(i: int) -> tuple[float, int, str | None]:
with lock:
model = next(order)
t0 = time.monotonic()
try:
tokens = _chat(url, model, PROMPTS[i % len(PROMPTS)], max_tokens, timeout)
except (urllib.error.URLError, OSError, ValueError) as exc:
return time.monotonic() - t0, 0, f"{model}: {exc}"
return time.monotonic() - t0, tokens, None

with concurrent.futures.ThreadPoolExecutor(concurrency) as pool:
list(pool.map(one, range(warmup)))
t0 = time.monotonic()
results = list(pool.map(one, range(requests)))
wall = time.monotonic() - t0
latencies = sorted(r[0] for r in results if r[2] is None)
errors = [r[2] for r in results if r[2] is not None]
tokens = sum(r[1] for r in results)

def pct(p: float) -> float | None:
return latencies[min(len(latencies) - 1, int(p * len(latencies)))] if latencies else None

return {
"concurrency": concurrency, "requests": requests, "models": models,
"max_tokens": max_tokens, "wall_s": wall, "output_tokens": tokens,
"output_tok_per_s": tokens / wall if wall else 0.0,
"latency_mean_s": sum(latencies) / len(latencies) if latencies else None,
"latency_p50_s": pct(0.5), "latency_p99_s": pct(0.99),
"failed": len(errors), "errors": errors[:5],
}


def main() -> int:
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--url", default="http://127.0.0.1:8100")
p.add_argument("--models", required=True, help="comma-separated; requests take them in turn")
p.add_argument("--concurrency", type=int, default=32)
p.add_argument("--requests", type=int, default=256)
p.add_argument("--warmup", type=int, default=None, help="default: one wave (= concurrency)")
p.add_argument("--max-tokens", type=int, default=128)
args = p.parse_args()
stats = closed_loop(
args.url, args.models.split(","), args.concurrency, args.requests, args.max_tokens,
warmup=args.concurrency if args.warmup is None else args.warmup,
)
print(json.dumps(stats, indent=1))
return 1 if stats["failed"] else 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading