Repository navigation
Phase 6: final benchmark vs. vLLM and SGLang - #8
Merged
Merged
Conversation
…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
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
torch==2.13.0/triton==3.7.1build, bf16 and int8, uniform and zipf routing, 7 token counts (1-2048). Zero results refused across all 224 combinations run.DeepseekForCausalLM->DeepseekV2ForCausalLMconfig shim both tuners needed, a publishedServerArgsSGLang'sfused_expertsrequires, a repeatability check widened forindex_add_'s non-bitwise-deterministicatomicAddon CUDA, a derived end-to-end latency for vLLM's--save-detailedschema (which carries no such field directly), a subprocessPATH-order fix, and a self-matchingpkillpattern that was silently killing its own SSH session.dispatch-persistent+int8), a self-poisoningmergeglob, several unguarded exceptions (ZeroDivisionError,StatisticsError,IndexError, rawKeyError) 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 -- seedocs/STATUS.md, "Task 15 step 5" for the full list with commit hashes.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):
quantized)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):
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):
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
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 --strictclean, 262 passed / 3 skipped / 2 deselected (pytest -m "not gpu")docs/findings/phase-6/with raw per-engine JSON evidence