Skip to content

Phase 6: final benchmark vs. vLLM and SGLang - #8

Merged
bsreecharanreddy merged 38 commits into
mainfrom
phase-6-final-benchmark
Sep 20, 2026
Merged

bsreecharanreddy merged 38 commits into
mainfrom
phase-6-final-benchmark

Conversation

@bsreecharanreddy

Copy link
Copy Markdown
Owner

Summary

  • At-scale correctness gate (owed since Phase 5b flagged Phase 5a's 29-position claim as too small): naive, persistent, and int8 all agree with the stock reference at 1,036 real-tokenizer positions -- 97.1-97.3% top-1 agreement, zero large-gap disagreements against the pre-registered 1.0-logit threshold. A stock-vs-stock control at the same 1,036 positions has zero disagreements of any kind, proving the measurement process itself introduces no spurious flips.
  • Kernel-level race: dispatch's naive and persistent Triton grouped-GEMM kernels vs. vLLM 0.29.0's and SGLang 0.5.20's own fused-MoE, on identical seeded inputs, the same GPU/model/torch==2.13.0/triton==3.7.1 build, bf16 and int8, uniform and zipf routing, 7 token counts (1-2048). Zero results refused across all 224 combinations run.
  • Labeled non-comparable engine-reference serving comparison: vLLM and SGLang each served the real model end-to-end at concurrency 1/4/16/64. dispatch has no serving stack of its own (system design §6, amended to say so explicitly); its concurrency-1 numbers are reported alongside, not folded into the ranked table.
  • Six real fixes shipped mid-session (all in the runbook): a DeepseekForCausalLM->DeepseekV2ForCausalLM config shim both tuners needed, a published ServerArgs SGLang's fused_experts requires, a repeatability check widened for index_add_'s non-bitwise-deterministic atomicAdd on CUDA, a derived end-to-end latency for vLLM's --save-detailed schema (which carries no such field directly), a subprocess PATH-order fix, and a self-matching pkill pattern that was silently killing its own SSH session.
  • Whole-branch review, done as a separate follow-up pass: found and fixed 20 real issues in this session's own new code (not the paid GPU results, which stand as measured) -- a reachable uncaught crash (dispatch-persistent + int8), a self-poisoning merge glob, several unguarded exceptions (ZeroDivisionError, StatisticsError, IndexError, raw KeyError) where the code's own docstrings promised a clean refusal, a silent no-op gate mode, non-deterministic table ordering, and a couple of duplicated-logic cleanups. All fixed, each its own commit -- see docs/STATUS.md, "Task 15 step 5" for the full list with commit hashes.
  • A wrong headline claim, caught and corrected while writing this PR: the original write-up said "dispatch's naive kernel wins at int8 128/512 tokens." Checking the raw measured JSON directly found that claim compared dispatch only against SGLang and dropped vLLM out of the comparison. Corrected across the findings doc, CLAUDE.md, README.md, STATUS.md, and CHANGELOG.md -- see "Results" below and the last commit for the full account.

Cost: $4.5448 of the $10 cap, measured from RunPod's billing API rather than rate x duration.

Results

Correctness gate (1,036 positions each):

config disagreements large-gap widest gap top-1 agreement
stock vs. stock (control) 0 0 0.000 100.000%
naive (bf16) 28 0 0.750 97.297%
persistent (bf16) 30 0 0.750 97.104%
int8 (quantized) 29 0 0.750 97.201%

Kernel race — vLLM beats dispatch at every one of the 28 measured (precision, token count, distribution) combinations, bf16 and int8 alike. Gap by token count (dispatch's best tuned variant vs. vLLM):

tokens bf16 ratio (dispatch/vLLM) int8 ratio (dispatch/vLLM)
1 2.4x slower 3.5-4.1x slower
16 1.3x slower 1.7x slower
128 1.1x slower 1.3x slower
2048 1.2-1.3x slower 1.2-1.3x slower

dispatch does beat SGLang specifically, at 128 and 512 tokens in both bf16 and int8 (e.g. int8 128 tokens: dispatch 1.628ms vs. SGLang 2.111ms vs. vLLM 1.232ms) -- a real result against one contestant, not the other.

Engine reference (served, not raced):

engine concurrency out tok/s p50 TTFT (ms) $/M output tok
vLLM 1 115.5 37.9 1.97
vLLM 64 1269.2 174.1 0.18
SGLang 1 108.6 54.9 2.10
SGLang 64 1180.4 135.8 0.19
dispatch (naive, concurrency-1 only, non-comparable) 1 21.04 -- --

vLLM leads SGLang by roughly 6-9% throughout. dispatch's own concurrency-1 number is a genuine ~1.8x speedup over the stock model's eager moe_infer (21.04 vs. 11.87 tok/s), consistent with Phase 1, but not placed in the ranked table above: no HTTP layer, no CUDA-graph capture, no continuous batching -- comparing it ms-for-ms against a served number would compare two different things under one column header.

Things worth a reviewer's attention

  • This is a real, disclosed loss for dispatch's kernel, not a caveat under a positive headline. The project's own correctness-before-speed and never-quote-an-unmeasured-number rules exist for exactly this outcome.
  • A wrong comparative claim slipped into the original write-up ("dispatch wins at int8 128/512") and sat in five committed files (findings doc, CLAUDE.md, README.md, STATUS.md, CHANGELOG.md) before being caught during this PR's own preparation, by recomputing the winner at every cell directly from the raw per-engine JSON rather than trusting the hand-written prose. Fixed in the last commit; no measured numbers changed, only the narrative built on top of them.
  • Full 7-point tuning of vLLM's and SGLang's own MoE tuners was not run -- confirmed live to cost ~18 minutes per token count with no partial credit on an early stop, several times the phase's budget. Both engines are reported at shipped default configuration only, a disclosed limitation decided with the user at a $4.22 checkpoint.
  • dispatch has no served, batched engine of its own to place in the same ranked concurrency table as vLLM/SGLang -- design doc §6 amended to say so explicitly rather than imply an apples-to-apples comparison.
  • Three commits from the original session (3acf14c, c31d1f4, 2536764) broke this repo's own same-commit-STATUS.md rule. Left as a documented gap in STATUS.md rather than rewritten via history rewriting, since nothing is pushed anywhere else and the risk of rewriting outweighed the paperwork gap itself.

Test plan

  • make check: lint clean, mypy --strict clean, 262 passed / 3 skipped / 2 deselected (pytest -m "not gpu")
  • Real GPU session: at-scale correctness gate, kernel race (7 token counts x 2 distributions x 2 precisions x 4 contestants), and the vLLM/SGLang serving reference, all recorded under docs/findings/phase-6/ with raw per-engine JSON evidence
  • Whole-branch review (20 findings, all fixed) plus a raw-data recheck of every headline comparative claim before opening this PR

…audit

Phase 5a ran on transformers 4.57.6 with finite, non-zero logit diffs, so it
is not the RoPE-bug degenerate-output artifact an earlier draft feared. What
stands is that 29 positions in one run cannot rule out the near-tie
sensitivity 5b found in the same int8 kernel. Resolved as an input to Phase 6:
its correctness gate measures agreement at scale on the benchmarked config.
…reference

Design doc section 6 assumed a dispatch server for the serving benchmark to
target; none exists, so an end-to-end race would measure HF-eager overhead
rather than the kernel. Phase 6 instead races the kernels like-for-like on
one L40 and reports vLLM/SGLang serving as a labeled reference. Amends
design section 6 and records the decision in STATUS.md.
…istered

15 tasks: 9 local TDD tasks (code verified in a scratch copy first), then the
paid L40 session and wrap-up. Pre-registers the at-scale gate's logit-gap
threshold (1.0, from Phase 5b's near-tie data) and the uniform-only tuning rule
before any GPU time. Amends the design where live checks corrected it: vllm and
sglang share a torch pin so one engines venv works, the per-engine bare-GEMM
diagnostic is dropped, and the gate is per config.
…s them alone

ruff 0.16 formats Python code fences in Markdown, and the plan's old/new edit
fragments are not standalone Python, so make lint was red on the plan commit.
The L40 was out of stock on 2026-09-19 (Secure, availability NONE, CUDA 13.0
hosts unavailable). The user chose to wait rather than substitute the L40S or
A40, to keep Phase 6 on the same GPU class as Phases 1 and 5a.
The first real GPU run showed dispatch's combine (index_add_, atomicAdd on
CUDA) is not bitwise repeatable: two identical calls differ by one bf16 ulp on
24% of elements. Compare within the reference tolerance instead, and
parametrize per engine so one failure cannot hide the others.
The first real SGLang run failed with "config namespace 'exec' not published"
(fused_experts reads get_exec(), which SGLang fills only from a published
ServerArgs), and every later test then failed on a second
initialize_model_parallel. Publish a ServerArgs in init_distributed() and make
it run once.
… gaps

vLLM 0.29.0's --save-detailed bench output carries no per-request
'latencies' key at all (confirmed live, first real serving run) -- only
per-request ttft and the inter-token gaps that follow it. Summing them is
the same quantity a 'latencies' key would have held.
…d cost

31 evidence files pulled from the pod and verified byte-for-byte against
the pod's own file list before termination (reference safetensors excluded
per .gitignore -- they are regenerable from run_baseline.py, not committed
blobs). Cost measured from RunPod's billing API: $4.5448 of the $10 cap.
…d/CHANGELOG

Findings doc states the outcome plainly: vLLM's and SGLang's own fused-MoE
beat dispatch's kernels at default config on nearly every tested shape, in
both bf16 and int8, a real disclosed loss rather than a reframed win. The
at-scale correctness gate passed for every kernel (97.1-97.3% top-1
agreement at 1,036 positions, zero large-gap disagreements), superseding
Phase 5a's 29-position claim. vLLM and SGLang both served the real model
correctly across concurrency 1-64; dispatch has no serving stack to place
in that same table. Runbook records what actually worked on the pod:
direct SSH instead of the PTY proxy, the pkill self-match bug, the
DeepSeek-V1 tuner shim, the SGLang ServerArgs fix, the PATH-order fix, and
the derived-latency fix for vLLM's real bench-serve schema.
- _token_rank() no longer collapses every out-of-range num_tokens onto
  the same rank; unregistered token counts now sort after registered
  ones, by their own value, so render_markdown's table order can no
  longer depend on Python's hash-seed-dependent set iteration.
- DEFAULT_BLOCK_M moves to engines/base.py and is now the single
  source registry.DEFAULT_BLOCK_MS, DispatchEngine's block_m default,
  and race_summary's own 'default' row selection all derive from,
  instead of three independently-hardcoded 16s that could silently
  drift apart.

Found by the whole-branch review of this branch's own Phase 6 work.
render_markdown's engine/tokens/distribution lookup was a dict
comprehension that silently kept whichever row happened to sort last
on a duplicate key. A results directory holding a stale re-run
alongside a fresh one (an easy mistake on a metered GPU pod) picked
one measurement with no diagnostic that a conflict existed at all --
now it is a clear refusal naming the offending cell instead.

Found by the whole-branch review of this branch's own Phase 6 work.
--glob's default (*-race-*.json) matched merge's own default output
filename (...-phase-6-race-summary.json), already true of the Phase 6
evidence committed under docs/findings/phase-6/ where the two live
side by side. A second merge in that directory would silently try to
treat the first merge's own JSON array of rows as a per-engine
{'config','results'} record and crash with a bare TypeError.

- merge's default --run-label no longer contains 'race', so it cannot
  self-match the default --glob going forward.
- _command_merge now validates each glob-matched file actually looks
  like a per-engine race record before summarizing, so any future
  naming collision fails with a clear message instead of a crash.

Found by the whole-branch review of this branch's own Phase 6 work.
…t a crash

engine.prepare_bf16/prepare_int8() was called with no exception
handling at all, once per engine before any per-case work. dispatch's
persistent kernel has no int8 path and raises ValueError from there
(dispatch_kernels.py) -- a reachable combination via valid CLI flags
(--engine dispatch-persistent --precision int8) that crashed the whole
process before any JSON was written, losing every other engine's
results too and contradicting this module's own docstring ('the
evidence survives').

Also fixes the adjacent IndexError: str(exc).splitlines()[0] indexed
unconditionally into a list that is empty whenever str(exc) is ''.

Found by the whole-branch review of this branch's own Phase 6 work.
… of ignoring it

--tuning-label was required=True for every --engine, but
race_summary.summarize_race() only reads config['tuning_label'] on the
non-dispatch branch -- for any dispatch-* engine, _dispatch_rows()
derives 'default'/'tuned' purely from the --block-ms sweep and never
looks at the flag at all. A user who passed --tuning-label default
expecting to suppress the sweep got no error and no effect.

Found by the whole-branch review of this branch's own Phase 6 work.
…bfloat16

init_distributed() hardcoded dtype="bfloat16" in the ServerArgs it
publishes via set_global_server_args_for_scheduler, regardless of
prepare_inputs's own --dtype {float16,bfloat16} flag. A float16 race
run against the sglang engine published a scheduler config claiming
bfloat16 while the real tensors flowing through fused_experts were
float16.

_command_run now reads the prepared manifest before dispatching to
sglang's setup and threads manifest["dtype"] through, instead of
re-reading the manifest a second time afterward only for the record's
config block.

Found by the whole-branch review of this branch's own Phase 6 work.
_installed_version() in run_engine_race.py and _version() in
phase6_engine_reference.py were byte-for-byte the same
try/importlib.metadata.version/except-PackageNotFoundError helper,
added in the same PR under different names. Moved to
engines/base.py, the module both Phase 6 driver scripts already
import from, so a future fix to the lookup only needs to happen once.

Found by the whole-branch review of this branch's own Phase 6 work.
summarize_bench_result mixed raw.get("failed", 0) (safe) with plain
indexing raw["errors"]/["output_lens"]/["ttfts"]/["itls"], and never
checked those arrays' lengths against completed. A missing 'errors'
key raised a raw KeyError instead of the function's own refusal;
arrays shorter than completed fed empty/short lists into
statistics.mean/percentile, which crash with StatisticsError or
IndexError instead of the intended clean ValueError. A future vLLM/
SGLang schema drift (or a hand-built raw dict for a new engine) hit
either path instead of the designed 'refusing to report numbers' exit.

Also guards output_tokens_per_s <= 0 before dividing by it for
cost_per_million_output_tokens_usd, previously an unguarded
ZeroDivisionError if a future bench client ever reports zero
throughput on a nonzero-completed run.

Found by the whole-branch review of this branch's own Phase 6 work.
(self.positions - self.disagreements) / self.positions had no guard
for positions == 0. _gap_split only validates the vocab dimension
(reference.shape[-1] < 2); a zero-length prompt's logits (shape
(0, vocab)) produce a GapSplitAgreement with positions=0 that raised
an unhandled ZeroDivisionError the moment .top1_agreement or
.to_dict() was read -- exactly at the point run_baseline.py is about
to persist the run's results.json. Now a clear, named ValueError.

Found by the whole-branch review of this branch's own Phase 6 work.
server_log was opened and popen_fn() was called before the try/finally
that writes the evidence JSON and closes the log handle. If popen_fn
itself raised (e.g. the engine binary isn't on the engines venv's
PATH) -- a real possibility this project's own cost-discipline section
warns about on a metered pod -- run_engine_reference propagated the
exception with zero evidence written and the just-opened log handle
leaked, contradicting this module's own docstring.

Also adds server.wait() after kill(): a server that ignores SIGTERM
proceeded straight from kill() to closing the log and writing evidence
with no confirmation the process actually exited, a timing race for a
caller that launches a second engine's server shortly after in the
same pod session.

Found by the whole-branch review of this branch's own Phase 6 work.
write_trace was always called with the module-level TRACE_LINES
constant (512), sized only to cover the hardcoded default
CONCURRENCIES tuple (max 64). --concurrencies has no upper bound tied
to that constant, so a caller passing a larger concurrency (e.g. 128,
needing 1024 prompts) requested more prompts from vllm bench serve
than the trace file actually contained.

run_engine_reference now sizes the trace from the concurrencies it was
actually given: max(num_prompts_for(c) for c in concurrencies).

Found by the whole-branch review of this branch's own Phase 6 work.
…ly no-op'ing

Enforcement (the MIN_GATE_POSITIONS floor and the large-gap-disagreement
refusal) only ran when gap_split was not None, which requires
--compare-reference -- but --prompt-set gate's own help text promised
it 'enforces its gap-split rule' unconditionally. Running
--prompt-set gate without --compare-reference (e.g. a copy-pasted
command with a dropped flag) completed successfully with gap_split:
null and gave the false impression the at-scale gate had run.

main() now refuses this combination before loading the model, instead
of after spending GPU time on a run that can't do what it says.

Found by the whole-branch review of this branch's own Phase 6 work.
_quantized_matmul() decided 'which backends have an int8 kernel'
inline (torch -> dequant reference, naive -> the real int8 kernel,
persistent -> raise), duplicating a backend-capability fact that
backends.py -- the module docstring already calls the single source of
truth for backend->kernel mapping -- didn't actually own for int8.

QUANTIZED_BACKENDS in backends.py now names the fact once; a future
int8-capable backend (or the persistent kernel someday gaining an
int8 path) is a one-tuple edit instead of an if/elif buried in the
engine adapter.

Found by the whole-branch review of this branch's own Phase 6 work.
…GELOG

Task 15 step 5 (whole-branch review) found 20 real issues across the
session's own new code (not the paid GPU results, which stand as
measured); all fixed, each its own commit, cited by hash in STATUS.md.
Test count 246 -> 262. Three earlier commits (3acf14c, c31d1f4,
2536764) are flagged in STATUS.md as having broken the same-commit
STATUS.md rule -- documented, not retroactively rewritten.
Checking the raw measured JSON directly while writing the PR body
found that the original session's own findings doc, and the copies of
its headline claim in CLAUDE.md, README.md, STATUS.md, and CHANGELOG.md,
all say dispatch's naive kernel 'wins' at int8 128 and 512 tokens. That
compared dispatch only against SGLang at those two points and dropped
vLLM out of the comparison -- vLLM's measured 1.232ms and 1.312ms at
128 and 512 tokens are both lower than dispatch's best tuned variant
(1.628ms, 1.725ms) the whole time.

The corrected, verified-against-raw-JSON picture: vLLM wins every one
of the 28 measured (precision, token count, distribution)
combinations, with no exception. The gap is largest at 1 token
(2.4-4.1x slower) and narrows to ~1.1-1.3x slower at 128 tokens and
up, never closing. dispatch does beat SGLang specifically at 128 and
512 tokens, both precisions -- a real result against one contestant,
not the other, and worth keeping, just not the claim that was written.

Also fixes the int8 table's bold markers (128/512 rows wrongly bolded
dispatch-naive instead of vllm) in the findings doc -- the underlying
numbers were always correct; only the bolding and prose were wrong.

No measured numbers changed. Nothing here touches code or tests.
@bsreecharanreddy
bsreecharanreddy merged commit 07c99d5 into main Sep 20, 2026
1 check passed
@bsreecharanreddy
bsreecharanreddy deleted the phase-6-final-benchmark branch September 20, 2026 16:19
bsreecharanreddy added a commit that referenced this pull request Sep 20, 2026
Branch phase-6-final-benchmark merged and deleted; STATUS.md, README,
CLAUDE.md, and CHANGELOG updated from 'PR pending' to the merged PR
link. Committed directly to main -- a doc-only bookkeeping update, no
code, make check green (262 passed, 3 skipped, 2 deselected).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant