Skip to content

Fix throughput-test: real root cause (context-length mismatch), switch to agentic-replay recipe#25

Open
aistackdev wants to merge 51 commits into
mainfrom
feat/throughput-config-snapshot
Open

Fix throughput-test: real root cause (context-length mismatch), switch to agentic-replay recipe#25
aistackdev wants to merge 51 commits into
mainfrom
feat/throughput-config-snapshot

Conversation

@aistackdev

Copy link
Copy Markdown

Summary

Two fixes bundled (grew out of the same investigation):

1. Config snapshot for InferenceX-app's ingest (framework/precision/tp/disaggregation into throughput artifacts, plus a 503-guard on the post-sweep redeploy re-check) -- see PR history for details, already validated live earlier in this branch's life.

2. The real root cause of "throughput-test never produces real numbers": it was never a duration/entries/worker-count tuning problem. Live investigation (curl reproduction against the endpoint + aiperf's own error_summary export) found the actual cause: semianalysis_cc_traces_weka's real Claude-Code coding-session traces (every trace ≥20 turns, cumulative context grows turn over turn) routinely exceeded the deployment's 32768-token max context, so nearly every request was rejected outright with HTTP 400 ("input longer than the model's context length") -- no amount of num-dataset-entries/conc-list/benchmark-duration-s tuning could ever fix a request-rejection problem.

Fixed via two changes together:

  • Deployment's model context raised to 128K (external change).
  • Switched utils/aiperf submodule to thangquang09/aiperf (benchtool/agentx-weka branch) and adopted the internal agentic-replay recipe (matches benchmark-tmpl.yml/run-sweep.yml) instead of the lightweight concurrency-ping: --public-dataset semianalysis_cc_traces_weka_with_subagents_060826, --scenario inferencex-agentx-mvp, --benchmark-duration 600, --use-server-token-count, --num-dataset-entries 64, --slice-duration 1.0, --max-context-length 120000, --unsafe-override. All wired as code defaults in run_throughput_test.py, overridable per-stack via throughput-tests.yaml (now much shorter -- everything has a sensible default).

Also added: a permanent (not diagnostic-only) error_summary surfacing from aiperf's raw export whenever it's non-empty, so a future silent-failure mode doesn't require re-deriving this from scratch.

Confirmed live, all 3 stacks, full production config (conc-list [1,8,16])

Real, comparable throughput data at conc=1 and conc=8 for all three:

  • sglang-vanilla: 1363 / 696 tok/s
  • sglang-mooncake-store: 1355 / 4789 tok/s (scales notably better at conc=8)
  • sglang-pd-disaggregation: 1346 / 2001 tok/s

conc=16 consistently disconnects/errors on all three (ServerDisconnectedError/ClientPayloadError) -- a real capacity ceiling finding for these deployments under sustained heavy agentic load, not a bug to chase further.

Test plan

  • Dispatched all 3 stacks against the full production config -- all succeeded with real conc=1/conc=8 data, conc=16 hits a genuine, consistent capacity ceiling (not a config or code bug).

Ngô Quang Hòa and others added 30 commits May 22, 2026 13:25
- runners.yaml: register `h100-1x` runner type with our GreenNode H100 node
- nvidia-master.yaml: add `gptoss-fp4-h100-1x-vllm` (openai/gpt-oss-20b at FP4,
  TP=1, conc 4-16) — sized to fit a single 80GB H100, used as a smoke test
- runners/launch_h100-greennode.sh: plain-Docker launch script for
  single-VM GPU boxes (existing scripts assume Slurm + enroot, which we
  don't have on GreenNode)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
callanjfox/kv-cache-tester → vngcloud/kv-cache-tester
cquil11/aiperf → vngcloud/aiperf

Lets a single fine-grained PAT scoped to vngcloud org authorize both
the main repo and submodule clones during actions/checkout, without
needing a two-step anonymous-checkout pattern. Pinned commit SHAs
already exist in the forks, so no submodule re-pin required.
- runners.yaml: register rtx5090-1x runner type pointing at our 5090 node
- nvidia-master.yaml: dsr1qwen3-bf16-rtx5090-1x-vllm config
  (deepseek-ai/DeepSeek-R1-0528-Qwen3-8B, BF16, TP=1, conc 4-16)
- runners/launch_rtx5090-greennode.sh: plain-Docker launcher
- benchmarks/single_node/dsr1qwen3_bf16_rtx5090.sh: vLLM serve + bench
  script for the 8B distill on a single 5090
- nvidia-master.yaml: 8k1k fixed-seq-len + agentic-coding scenarios
  added to gptoss-fp4-h100-1x and both dsr1qwen3-rtx5090 configs.
  8k1k single-node entries auto-trigger lm-eval at median/highest conc
  via InferenceX's mark_eval_entries logic — so this also enables eval.
- New dsr1qwen3-fp8-rtx5090-1x-vllm config (vLLM on-the-fly FP8 quant).
- Agentic scripts for dsr1qwen3 on RTX 5090 (bf16 + fp8 variants).
  TORCH_CUDA_ARCH_LIST=12.0 for sm_120 Blackwell consumer.
- dsr1qwen3_bf16_rtx5090.sh: MAX_MODEL_LEN now respects the env var
  (${MAX_MODEL_LEN:-9472}) so 8k1k fits.

Matrix: 30 entries (gptoss h100-1x: 10, dsr1qwen3-bf16 5090: 10,
dsr1qwen3-fp8 5090: 10 — each = 6 fixed-seq-len + 4 agentic).
Mirrors the 1x config (same gpt-oss-20b model, image, scenarios) but
TP=2 on h100-greennode_01. Gives direct apples-to-apples scaling
comparison: 1x H100 (TP=1) vs 2x H100 (TP=2) on identical workload.
The single character at EOF made the script exit 127 after every
successful gpt-oss benchmark, so every gptoss-fp4-h100-1x/-2x job in
run 26285383625 ran to completion (full TTFT/TPOT emitted) then was
marked failed. dsr1qwen3 jobs on rtx5090 were unaffected (different
launch script).
Native Gemma 4 MTP via vLLM v1 --speculative-config (PR
vllm-project/vllm#41745, merged 2026-05-08). Targets the 2x H100
GreenNode box (h100-greennode_01), uses google/gemma-4-31B-it
target + google/gemma-4-31B-it-assistant drafter (Q-only decoder
sharing target's KV cache, ~few hundred MB extra weights, no
separate process).

Image pinned to v0.22.0 as the first vLLM release post-PR.
Drafter HF slug is the presumed 31B name following the
E2B/E4B/26B pattern from the PR test plan — may need adjusting
if Google's actual upload differs.

Sweep: 1k1k + 8k1k * conc {4,8,16}, all spec-decoding: mtp.
Comparable to the MEP-0002 prior-art Gemma 4 baseline.
v0.22.0 doesn't exist on Docker Hub. v0.21.0 (tagged 2026-05-15) is
the first stable release after PR #41745 was merged (2026-05-06), so
it contains the Gemma 4 MTP code path. After v0.21.0 vLLM only ships
nightly builds.
aiperf SystemController times out at 'Configure Profiling' on all
GreenNode runners. Commented out the agentic-coding blocks for:
  gptoss-fp4-h100-1x-vllm
  gptoss-fp4-h100-2x-vllm
  dsr1qwen3-bf16-rtx5090-1x-vllm
  dsr1qwen3-fp8-rtx5090-1x-vllm
Restore once #2 is resolved. The fixed-seq-len scenarios still run.
Gemma 4 is multimodal; vLLM startup fails with:
  ValueError: max_tokens_per_mm_item (2496) is larger than
              max_num_batched_tokens (2048)
because the vision encoder emits 2496 tokens per image and chunked
MM input is disabled by default. 8192 covers MM items and also
matches our 8k1k workload upper bound.
Rename gemma4-fp8-h100-2x-vllm-mtp → gemma4-fp8-h100-2x-vllm-bench
and add 'spec-decoding: none' rows alongside the existing
'spec-decoding: mtp' rows. Matrix now emits 12 entries (6 mtp + 6
none) per dispatch, so MTP-vs-baseline rows share a wall-clock
window — no between-run drift in HF mirror speed, runner thermal,
or vLLM patch level.

Also fix unrelated YAML typo on line 17 (prefill:wr → prefill:)
that was blocking matrix validation.
Validator forbids extra fields on single-node search-space entries,
so num_speculative_tokens can't ride the matrix entry directly.
Workaround: distinct model-prefix per N, the script derives N from
$MODEL_PREFIX. Script filename variants (gemma4n4_fp8_h100.sh,
gemma4n6_fp8_h100.sh) symlink to gemma4_fp8_h100.sh.

Also drop the broken /workspace/spec_metrics_*.txt curl — server.log
(already uploaded by the workflow's Upload server logs step) already
captures vLLM's 'SpecDecoding metrics:' lines every ~10s during
inference, so the curl was redundant. It was also broken: workspace
gets wiped by next job's actions/checkout 'clean: true', so only the
last job's file survived (empty).
…s search-space fields

Both knobs were previously inexpressible in the matrix schema:
- N was smuggled into model-prefix (gemma4n4/n6) and decoded by a case
  statement in gemma4_fp8_h100.sh — this polluted infmax_model_prefix in
  agg_bmk.json with fake "models" and forced any consumer to string-parse
  the suffix to recover N.
- max-num-batched-tokens was hardcoded to 8192 in the bench script.

Adds them as typed Optional[int] fields on SingleNodeSearchSpaceEntry +
SingleNodeMatrixEntry, plumbs through generate_sweep_configs.py, the
workflow templates (benchmark-tmpl.yml inputs + env + RESULT_FILENAME
slug so concurrent jobs at different knob values don't collide),
launch_h100-greennode.sh RUN_ENV, the bench script, and
process_result.py (emits flat columns in agg_bmk.json) +
compare_results.py (DB lookup key).

Collapses gemma4-fp8-h100-2x-vllm-bench-{n4,n6} back into the single
gemma4-fp8-h100-2x-vllm-bench key with N=2/4/6 as search-space rows,
and deletes the n4/n6 script symlinks. Adds new
gemma4-fp8-h100-1x-vllm-bench (TP=1, mnbt sweep 4k/8k/16k) as the
first config exercising the new max-num-batched-tokens knob.

Dashboard handoff: agg_bmk.json rows gain two optional int columns
(num_speculative_tokens, max_num_batched_tokens). RESULT_FILENAME slug
gains _n<value>_mnbt<value> segments. compare_results.build_config_params
passes both fields as DB lookup params for the comparator to consume
once the schema is migrated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vLLM's APIServer / EngineCore are launched via multiprocessing.spawn,
which starts fresh Python interpreters. Those re-import huggingface_hub
and call get_token(); if HF Hub was imported before the env var was
read on a given code path, the worker emits
  "You are sending unauthenticated requests to the HF Hub ..."
even though the parent shell has HF_TOKEN set (confirmed via the
`docker run -e HF_TOKEN` plumbing in launch_*.sh + benchmark-tmpl.yml).

Writing the token to ~/.cache/huggingface/token sidesteps env-var
propagation entirely — every HF Hub code path falls back to that file.
Also exports HUGGING_FACE_HUB_TOKEN for legacy library paths.

Auto-invoked at source time so every bench script that does
  source "$(dirname "$0")/../benchmark_lib.sh"
gets the fix without having to add a call. No-op when HF_TOKEN is
unset (local smoke runs without GHA secrets).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Qwen3.5-27B (dense) FP8 on the single H100 GreenNode box (h100-1x ->
h100-greennode_00), vLLM v0.21.0. Mirrors the gemma4-fp8-h100-1x-vllm-bench
sweep: isl {1k,8k} x osl 1k x conc {4,8,16}, sweeping max-num-batched-tokens
4k/8k/16k (18 jobs total).

Distinct model-prefix (qwen3.5-27b) so the inferencex.com dashboard does not
conflate this dense model with the existing qwen3.5-397B-A17B MoE configs.

HF slug Qwen/Qwen3.5-27B-FP8 is the presumed name following the
<repo>-<size>-FP8 pattern; confirm against Qwen's actual upload before dispatch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The config qwen3.5-27b-fp8-h100-1x-vllm-bench (d5e5dc1) had no matching
launch script, so the runner died at "Launch job script" with exit 127
(benchmarks/single_node/qwen3.5-27b_fp8_h100.sh: not found).

Add it as a vLLM single-H100 script modeled on gemma4_fp8_h100.sh:
- vLLM auto-detects fp8 from the Qwen/Qwen3.5-27B-FP8 checkpoint's
  quantization_config (native fp8, dynamic activation scheme), so no
  --quantization flag; --dtype bfloat16 sets the compute dtype.
- Serve flags per the production config: --reasoning-parser qwen3,
  --enable-auto-tool-choice, --tool-call-parser qwen3_xml,
  --gpu-memory-utilization 0.90, --trust-remote-code, TP=1.
- max-model-len + max-num-batched-tokens threaded from the matrix via
  config.yaml so the mnbt 4k/8k/16k sweep works; --max-num-seqs=$CONC.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SGLang counterpart to gemma4-fp8-h100-2x-vllm-bench so Gemma 4 31B can be
compared vLLM-vs-SGLang on the same 2x H100 / TP=2 baseline (spec-none).
No SGLang gemma4 config existed before; gemma4 had only ever run on vLLM.

Three pieces:
- runners/launch_h100-greennode.sh: prefer a framework-tagged single_node
  script (<prefix>_<prec>_h100_<framework>.sh) with a fallback to the
  historical engine-less name. Lets two engines coexist for one model on
  this box (b200/b300 launchers already do this). NO spec suffix is added:
  scripts like gemma4_fp8_h100.sh branch internally on $SPEC_DECODING, and
  no *_h100*_vllm.sh scripts exist, so every current vLLM config (gemma4,
  qwen3.5-27b, gptoss) keeps falling back to its legacy name unchanged.
- benchmarks/single_node/gemma4_fp8_h100_sglang.sh: SGLang launch, on-the-fly
  fp8 (--quantization fp8) + fp8 KV to mirror the vLLM bench, TP from matrix.
- gemma4-fp8-h100-2x-sglang config: 6 jobs (isl{1k,8k} x conc{4,8,16}).

MTP omitted on purpose — SGLang's gemma4 MTP path differs from vLLM's
--speculative-config drafter; this is baseline-vs-baseline. First dispatch
will also confirm whether SGLang v0.5.12 supports the Gemma 4 arch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documents the 7-layer path from a config entry in
.github/configs/*-master.yaml through generate_sweep_configs.py,
e2e-tests.yml, benchmark-tmpl.yml, runners/launch_<family>.sh, to the
benchmarks/single_node/*.sh launch script and the dashboard.

Captures the edit recipes for the common changes (engine, sweep params,
card count, new model), the derived launch-script naming rule, the two
silent-failure rules (a new config needs a matching launch script or it
dies at "Launch job script" with exit 127; runner != card count), the
HF-slug/arch pre-checks, local validation commands, the vngcloud dispatch
command, and run-watching/debugging tips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First SGLang dispatch confirmed SGLang v0.5.12 DOES support Gemma 4
(gemma4_mm.py/gemma4_vision.py load, server starts), but on-the-fly
--quantization fp8 crashes in the vision tower: gemma4_vision.py forward
-> fp8 apply -> triton_scaled_mm assert (scale_b.shape mismatch), so the
scheduler dies and the server never becomes healthy.

Fix: serve the pre-quantized RedHatAI/gemma-4-31B-it-FP8-dynamic
checkpoint (compressed-tensors, ignore=['re:.*vision.*','lm_head',
're:.*embed_tokens.*']) and drop --quantization. SGLang auto-detects
compressed-tensors and honours the ignore list, keeping the vision
encoder in bf16 while running the LLM in fp8 — exactly "fp8 LLM, vision
unquantized". Slightly different fp8 recipe than the vLLM bench's
on-the-fly fp8 (noted in the config comment).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cut the prose walkthrough, HF curl recipes, verbose flow diagram and Notes
section; keep the load-bearing facts (flow, field reference, derived
script-name rule, edit table, the two silent-failure rules, validate +
dispatch commands, engine gotchas). ~196 -> ~60 lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nch)

SGLang baseline for Qwen3.5-27B on a single H100, mirroring the vLLM
qwen3.5-27b-fp8-h100-1x-vllm-bench on the SAME checkpoint
(Qwen/Qwen3.5-27B-FP8) for an engine-to-engine comparison. 6 jobs
(isl {1k,8k} x conc {4,8,16}); no max-num-batched-tokens sweep since
that's a vLLM knob (SGLang uses --chunked-prefill-size, fixed 8192).

Launch script qwen3.5-27b_fp8_h100_sglang.sh adapted from the 397B-A17B
qwen3.5_fp8_h100.sh: dense single-GPU (dropped expert-parallel-size and
the multi-GPU flashinfer allreduce fusion), and — crucially — NO
--quantization. Qwen/Qwen3.5-27B-FP8 is a pre-quantized block-fp8
checkpoint whose quantization_config.modules_to_not_convert already
excludes the whole vision tower (model.visual.*), lm_head, embeddings and
the linear-attn conv1d/in_proj layers; SGLang auto-detects that and keeps
the vision encoder in bf16. Forcing on-the-fly fp8 would quantize the
vision tower and crash in triton_scaled_mm (the gemma4 failure mode).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-ingest)

Dashboard ingests only runs you mark ready. Document the two paths:
[ingest] commit-prefix convention for the 15-min auto-scan (with the
current startswith caveat), and the force-ingest workflow_dispatch by
run id that works today.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The auto-ingest concurrency group (cancel-in-progress: false) keeps only
one running + one pending, so dispatching several run_urls at once gets
the middle ones cancelled. Dispatch sequentially.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rd or rows skip

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add dev to the push and pull_request branch filters in run-sweep.yml so
the perf sweep fires on dev as well as main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add AIPerf benchmark client integration

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: route gemma4 h100 sweep to 2x runner

* test: narrow gemma4 h100 sweep

* test: benchmark gemma4 with aiperf only

* docs: add aiperf integration guide

* chore: remove local h100 launcher docs

* Trigger Run Sweep when a PR is opened

Add the 'opened' pull_request type so the sweep fires as soon as a
non-draft PR is opened (still gated by the perf-changelog.yaml path
filter), not only on ready_for_review/synchronize/label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: AIPerf agentic-replay for 3 trace datasets (+ optional --tokenizer) (#9)

## What

Integrates the **agentic-replay** scenario type into the InferenceX AIPerf path and wires up three trace datasets, so a recorded `mooncake_trace` JSONL can be replayed once (closed-loop, with think-time) or driven back-to-back as a capacity sweep — all through official AIPerf v0.9.0.

## Datasets added (`benchmarks/single_node/agentic/datasets/`)
- `minimax_claude_code_prod_v3.jsonl` — Claude-Code MiniMax production trace (think-time)
- `agentic_coding_1variant_64k_150s.jsonl` — synthetic agentic-coding 64k tier (think-time)
- `gemma_blend_prod.jsonl` — Gemma blend_prod, single-turn back-to-back (`strip-trace-delays`)

## Plumbing
- **Adapter** (`utils/bench_serving/aiperf_adapter.py`): duration-mode stop condition, `--no-fixed-schedule`, `--inter-turn-delay-cap-seconds`, `--dataset-sampling-strategy`, `--benchmark-grace-period`, goodput, and an optional **`--tokenizer`** passthrough (defaults to the served model when unset).
- **Matrix/validation** (`generate_sweep_configs.py`, `validation.py`): `agentic-replay` scenario + `tokenizer` field, both flowing through `e2e-tests.yml` → `benchmark-tmpl.yml` → launcher → adapter.
- **Launcher** (`qwen3-4b-2507_bf16_h100_vllm.sh`, `runners/launch_h100-greennode.sh`): trace-subset (`#N`), `STRIP_TRACE_DELAYS`, duration-based replay, canonical `REPLAY_ARGS`.
- **Configs**: three `*-smoke*` keys on the qwen3-4b-2507 path + matching `perf-changelog.yaml` entries.

## Docs / tests
- ADR-0002 (keep both agentic paths: think-time vs back-to-back) + MVP-vs-Mode1 note.
- `agentic-replay-run` Claude skill (runbook; inherits config mechanics from `bench-config`).
- Unit tests: adapter (tokenizer set/omit), validation, sweep-config, process_result — green.

## Validation
3 datasets validated live at conc=4 on Qwen3-4B (h100-greennode_00): prefix-cache hit MiniMax prod **51.5%**, 64k 1-variant **95.7%**, Gemma blend b2b **3.5%** — spanning the structural spectrum.

* feat(aiperf): add utils/aiperf-mooncake submodule (clean v0.9.0 fork)

Second aiperf submodule (utils/aiperf-mooncake) pinned to a clean v0.9.0
fork (thangquang09/aiperf) wired via AIPERF_SOURCE_DIR so the mooncake
agentic-replay path installs our fork instead of stock PyPI 0.9.0. The
weka path (utils/aiperf) is unaffected. See ADR-0003.

Also update the agentic-replay-run skill to require the AIPERF_SOURCE_DIR
export in the launch script for all three datasets, and CONTEXT.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(skill): add optional DCGM toggle to agentic-replay-run

Ask the user (Q6) whether to enable DCGM; when yes, the agent edits the
runner launch script to start a dcgm-exporter sidecar (--network host) so
AIPerf reaches localhost:9400/metrics for richer GPU telemetry. gpu_metrics.csv
unchanged. Includes the copy-paste bash block and first-run port-9400 check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(agentx-weka): Qwen3-4B vLLM launcher + agentx-weka-run skill

Adds the agentx-weka (inferencex-agentx-mvp / cc-traces-weka corpus) path
for vLLM on h100-greennode, validated by a 90s smoke at conc 2/4 (0 errors).

- benchmarks/single_node/agentic/qwen3-4b-weka_bf16_h100_vllm.sh: launcher
  that isolates aiperf in a clean venv so its anyio/starlette upgrade never
  reaches vLLM's system python (v0.21.0 crashes otherwise).
- benchmark_lib.sh: WEKA_NUM_DATASET_ENTRIES caps the 949-trace corpus
  (default 949, backward-compatible).
- nvidia-master.yaml: qwen3-4b-weka smoke config (no perf-changelog entry, so
  it does not auto-trigger a sweep; dispatch manually).
- .claude/skills/agentx-weka-run: skill documenting the path + the venv /
  ordering / trace-load / disk gotchas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(aiperf): bump aiperf-mooncake submodule to include non-finite metrics fix

Bumps utils/aiperf-mooncake 524fac8b -> 1d2377f5, which merges a backport of
upstream ai-dynamo/aiperf#1025 onto the pinned benchtool/aiperf-0.9.0 line.

Without the fix, SGLang's NaN sglang:fwd_occupancy gauge (uninitialized before
the first forward pass) slips past the old `== float("inf")` filter, gets
orjson-encoded as JSON null, fails ServerMetricsRecordMessage validation, and
silently drops the ENTIRE scrape — taking cache_hit_rate / cached_tokens with
it. The agentic-replay path (/agentic-replay-run, mooncake_trace) scrapes
SGLang /metrics with --enable-metrics, so it was affected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(skill): agentic-replay-run smoke uses 2-request warmup

Default 20-request warmup runs before profiling and can eat a short
smoke window (observed ~750s on 31B/131072-ctx). For duration 90, set
--warmup-request-count to 2 in the launch script (not env-plumbed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [dataset] add minimax CC v4 weka-format dataset (234 traces, hackathon week)

Production-grounded agentic-coding dataset from vMonitor gateway logs:
- Date range: June 10-17, 2026 (hackathon week)
- Full capture: 260 sessions pulled, 234 traces (>=2 turns), 21,449 requests
- Keyed by x-claude-code-session-id (real CC conversations)
- Weka trace format: block_size=64, LCP-based hash_ids
- Compaction modeled via reset_context (>10% context change, 6.2% reset rate)
- Concurrent bursts linearized
- think_time stored RAW (no cap) — runtime capping via scenario config
- Latency: latencies.request (full wall-clock)
- Validated: pydantic WekaTrace 234/234 pass, cache 86.5%

Stats: p50=37 turns, ISL p50=63K, OSL p50=121, think_time p50=0.8s p90=47.6s

* feat(agentic): add minimax v4 weka smoke

* chore(runner+skill): add DCGM sidecar to h200-greennode launcher; update agentic-replay-run skill

- runners/launch_h200-greennode.sh: DCGM exporter sidecar always-on before docker run
- .claude/skills/agentic-replay-run/SKILL.md: centralize flow, DCGM default-on,
  flexible yyyy/mm/dd run-naming pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* submodule+skill(aiperf-mooncake): switch to benchtool/agentx-weka; weka via aiperf-mooncake

utils/aiperf-mooncake now tracks benchtool/agentx-weka (forked from ajc/agentx
in the thangquang09 repo), which carries the weka_trace loader AND the
data_collector math.isfinite NaN filter. This lets weka_trace run through the
agentic-replay path on the same submodule as mooncake_trace, with no SGLang
runtime patch needed.

- .gitmodules: aiperf-mooncake branch benchtool/aiperf-0.9.0 -> benchtool/agentx-weka
- pointer: 1d2377f5 -> a75c4612
- agentic-replay-run skill: weka_trace AIPerf source utils/aiperf -> utils/aiperf-mooncake;
  fork-pin section + confirm-fork log note updated
- datasets README: same source update + rationale

Do not use utils/aiperf (vngcloud fork) for weka any more: its data_collector.py
uses '== float("inf")' which never catches NaN, so SGLang's fwd_occupancy=NaN
drops the entire /metrics scrape and would require the runtime patch
(patches/aiperf-skip-nonfinite-server-metrics.patch).

---------

Co-authored-by: Thắng. Lý Quang (5) <thanglq5@vng.com.vn>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Ngô Quang Hòa <hoanq3@vng.com.vn>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
aistackdev and others added 21 commits July 12, 2026 18:39
…put) (#15)

* vendor: bring in aiperf_adapter.py from dev for smoke-test throughput probe

Cherry-picking the smoke-test matrix onto a branch off main (rather than
merging all of dev, which carries ~30 unrelated commits) needs this one
dependency: utils/bench_serving/aiperf_adapter.py, the team's standard
aiperf wrapper the throughput probe shells out to. It's stdlib-only
(argparse/json/subprocess/pathlib), no other dev-only dependencies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* design: post-deploy smoke-test matrix (input/process/discovery)

Documents the plan for a new correctness+throughput check against live
inference-cicd deployments, keyed off its /discover self-report endpoint
rather than a hand-maintained catalog. Confirmed live: only sglang-vanilla
is currently registered/discoverable; sglang-mooncake-store and
sglang-pd-disaggregation exist in-cluster but have no public Ingress yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* design: use aiperf (not benchmark_serving.py) for smoke-test throughput

Corrects the throughput probe design to reuse utils/bench_serving/
aiperf_adapter.py + benchmark_lib.sh's ensure_aiperf (both on dev), per
the team's standing decision to standardize throughput checks on aiperf.
Confirmed the adapter's synthetic isl/osl mode works standalone against
any --url with a plain PyPI install -- no aiperf-mooncake submodule, no
self-hosted benchmark-client runner, no Docker required for this path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* design: all 3 stacks now registered in /discover

sglang-mooncake-store and sglang-pd-disaggregation are publicly reachable
and self-reporting now, closing the gap flagged in the original design.
Smoke-test matrix should cover all three from the start. Noted
sglang-pd-disaggregation's schema quirk (extra disaggregation field,
flat tp that doesn't split prefill/decode) as a non-blocking follow-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: rewrite smoke-test design docs as current-state only

Drop the past-vs-now narration (revision notes, strikethrough, "updated
from an earlier version of this doc") that accumulated across iterative
edits -- keep only what's true now, for readability.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* design: note tokens/watt needs a not-yet-exposed DCGM endpoint

aiperf already supports --gpu-telemetry-url for power-normalized metrics
(aiperf_adapter.py threads it through), and a DCGM exporter does run in
the cluster (gpu-operator namespace, port 9400) -- but it's ClusterIP-only
and not in /discover's payload for any of the 3 stacks. Tokens/GPU-count
doesn't need this (just throughput / discover.tp); tokens/watt does.
Documented as a pass-through-if-present, skip-if-absent field so wiring
it up later is additive, not a redesign.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* design: /discover now exposes gpu_metrics_url -- but nodes are multi-tenant

inference-cicd added a top-level gpu_metrics_url with a per-node DCGM
index. Verified live: sglang-pd-disaggregation has a dedicated node (safe
to wire up now), but sglang-vanilla and sglang-mooncake-store share a
4-GPU node -- aiperf's --gpu-telemetry has no pod-label filter, so pointing
it at that shared node's URL would mix in the other stack's (and an idle
GPU's) power draw. Proposed fix: ask inference-cicd for a per-stack
gpu_metrics_url (same convention as version_url), pre-filtered, so aiperf's
native ingestion still works unchanged rather than hand-filtering DCGM
lines ourselves.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* design: inference-cicd added per-stack gpu_metrics_url -- gap closed

Verified live: each /discover stack entry now has its own
gpu_metrics_url (/gpu-metrics/by-stack/<name>), pre-filtered
server-side to that stack's own pod's GPU lines -- confirmed for
sglang-vanilla and sglang-mooncake-store (which share a physical 4-GPU
node) that the other stack's GPUs and an idle GPU are correctly
excluded. Wires straight into aiperf --gpu-telemetry-url for all three
stacks now, no InferenceX-side filtering needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat: add smoke-test workflow (metadata + tool-calling probes)

New post-deploy correctness check against inference-cicd's live stacks,
per design/smoke-test-matrix.md. Triggered by repository_dispatch
(stack-deployed) or manual workflow_dispatch. The matrix is built at run
time from inference-cicd's live /discover endpoint, cross-referenced
against .github/configs/smoke-tests.yaml (which only declares which
probes to run and expected-metadata for drift checks -- never
base_url/model/framework/tp, which come from /discover live).

Two probes wired up so far, both verified against the live cluster:
- metadata: diffs version_url's live report against smoke-tests.yaml's
  `expect` block.
- tool-calling: sends a real tool-enabled chat completion, asserts a
  tool_calls response. Currently fails against all 3 live stacks (no
  tool-call parser configured server-side for this model) -- a genuine
  finding, kept as a hard failure per team decision, not papered over.

throughput probe (aiperf-based) intentionally not wired in yet -- next
commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat: add aiperf-based throughput probe to smoke test

Wires the throughput probe into the smoke-test workflow, per
design/throughput-test.md: a short aiperf concurrency sweep against the
live endpoint via utils/bench_serving/aiperf_adapter.py (the team's
standard aiperf wrapper), with everything about where to send requests
(--url/--endpoint/--endpoint-type/--model/--gpu-telemetry-url) derived
from the live /discover entry -- only isl/osl/concurrency/duration come
from smoke-tests.yaml.

Includes a redeploy guard: snapshots version_url before and after the
sweep and fails the probe if they differ, rather than silently
reporting throughput numbers that mixed two deployments.

Verified end-to-end against the live cluster (sglang-vanilla, conc=1,
8s duration): aiperf_adapter.py runs correctly, GPU telemetry
pass-through works, result JSON parses, redeploy guard reports false as
expected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Ngô Quang Hòa <hoanq3@vng.com.vn>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
The throughput probe computed real aiperf numbers (tokens/sec, TTFT, ITL
per concurrency) but nothing surfaced them anywhere -- confirmed by
inspecting the first live CI run (29191249678): the step summary only
showed a one-line "completed sweep at conc=[...]" and the underlying
data was silently discarded.

- run_smoke_test.py now renders a per-concurrency numbers table in the
  Markdown summary alongside the pass/fail table.
- Adds --results-file to write the full raw probe results (all data,
  not just pass/fail) as JSON, uploaded as a build artifact per stack.

Verified locally end-to-end against the live cluster (sglang-vanilla,
conc=[1,4]): summary table renders correctly, results JSON has full
per-concurrency aiperf output.

Co-authored-by: Ngô Quang Hòa <hoanq3@vng.com.vn>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Per sync with InferenceX-app (2026-07-12): they'll pull/ingest
run_type: live-check results into their own tab, so this repo doesn't
need to call trigger-ingest itself.

Co-authored-by: Ngô Quang Hòa <hoanq3@vng.com.vn>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…coding-trace dataset (#18)

Smoke test (metadata + tool-calling) and throughput test are two unrelated
checks -- throughput was a bundled probe using tiny synthetic isl/osl
padding, undersized and prone to intermittent HF Hub rate-limit crashes
under smoke-test.yml's parallel matrix. It's now a standalone
throughput-test.yml workflow using aiperf's semianalysis_cc_traces_weka
public dataset (real Claude Code coding-session traces) for a richer
signal, with HF_TOKEN wired in from the start.

Co-authored-by: Ngô Quang Hòa <hoanq3@vng.com.vn>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
… submodule, not PyPI (#19)

First live run (29196983273) failed at conc=1: pip's aiperf==0.9.0 is
upstream NVIDIA's package and rejects the dataset name outright --
semianalysis_cc_traces_weka is a vngcloud/aiperf fork addition, only
available via an editable install of the utils/aiperf submodule (same
pattern as benchmark_lib.sh's install_agentic_deps()).

Co-authored-by: Ngô Quang Hòa <hoanq3@vng.com.vn>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
… AIPerf metrics (#20)

* Add a subprocess timeout to the throughput-test aiperf call

A cancelled GH Actions run (29197167756) got stuck ~30min with logs never
persisted -- GH's cancel signal doesn't reliably interrupt this synchronous
subprocess call. Add a bounded timeout (benchmark-duration + 600s setup/
drain buffer) so a stuck aiperf invocation raises a clear error instead of
hanging silently and unobservably.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* TEMP diagnostic: shrink sglang-vanilla throughput test to isolate hang (not for main)

* Handle AIPerf metric blocks that are legitimately absent (not null)

AIPerf's export schema marks every latency/throughput metric block as
optional and omits it from the JSON (rather than serializing null) when a
run doesn't produce enough samples to compute it -- e.g. inter_token_latency
needs multi-token completions, which a short-duration/low-concurrency/
low-dataset-count window may not produce. aiperf_adapter.py's build_result()
assumed every block was always present and crashed with a bare KeyError,
which silently killed the whole concurrency sweep on the first sparse point
instead of just omitting that stat.

* Revert "TEMP diagnostic: shrink sglang-vanilla throughput test to isolate hang (not for main)"

This reverts commit 95e5c4f.

---------

Co-authored-by: Ngô Quang Hòa <hoanq3@vng.com.vn>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…unner (#21)

* Run throughput-test's sweep job on our self-hosted benchmark-client runner

The last two workflow_dispatch attempts on ubuntu-latest both hit
"The runner has received a shutdown signal" mid-sweep -- confirmed via the
GitHub API this is GitHub's own hosted-runner fleet
(runner_group_name: "GitHub Actions"), not something we can inspect or fix
from our side. Switch to our own bench-client_00 self-hosted runner
(labels: self-hosted, benchmark-client), which is already online and used
elsewhere for aiperf-based client benchmarking, so a long-running sweep
against a live endpoint isn't at the mercy of hosted-runner churn.
get-jobs stays on ubuntu-latest -- it's a quick /discover call with no
long-running step to lose.

* Fix pip invocation for the self-hosted runner (no bare pip on PATH)

bench-client_00 is a bare host, not ubuntu-latest's preconfigured Python
env -- 'pip: command not found'. Use python3 -m pip --break-system-packages,
matching benchmarks/benchmark_lib.sh's existing install helpers on the same
runner.

* Bootstrap pip on the self-hosted runner (python3 has no pip module at all)

bench-client_00 failed with 'No module named pip' -- python3 -m pip isn't
just missing from PATH, the pip module itself isn't installed. Bootstrap
via ensurepip first, falling back to get-pip.py if ensurepip itself was
stripped from this Python build.

* Add ~/.local/bin to PATH for the aiperf console script

Same non-root user constraint as the pip bootstrap: pip installs console
scripts under ~/.local/bin on this runner since it can't write to the
system bin dir, but that's not on PATH by default -- 'aiperf' (invoked
directly as a subprocess command, not a module) was FileNotFoundError.

* TEMP: add runner health diagnostic step (not for main)

* Revert "TEMP: add runner health diagnostic step (not for main)"

This reverts commit be225f2.

* Cap aiperf worker count to avoid OOM-killing the benchmark-client runner

Confirmed via SSH + journalctl/dmesg: aiperf's default worker auto-scaling
(min(concurrency, cpu_count*0.75-1), capped at 32) spawned enough worker
processes on our 16-core bench-client host that the sweep grew to ~31.6GB
RSS and got OOM-killed by the kernel -- twice, on two different runner
registrations on the same physical host, taking the whole runner service
down each time (systemd: 'Failed with result oom-kill').

Add --max-workers passthrough to aiperf_adapter.py and cap it at 4 for this
workflow's sweep -- it's a lightweight live-check, not a from-scratch sweep,
and doesn't need aiperf's full auto-scaled worker pool to sustain
concurrency up to 32.

* TEMP diagnostic: shrink sglang-vanilla to num-dataset-entries=10 to isolate memory scaling (not for main)

* Lower num-dataset-entries default to reduce OOM risk on the self-hosted runner

Replaces the single-stack TEMP diagnostic override. Applies num-dataset-entries:
20 to all 3 stacks (down from the 100 default) as a conservative stopgap --
this workflow also fires automatically via repository_dispatch on every
deploy, so leaving it at a value known to OOM-kill the runner would crash it
on every future deploy, not just manual test dispatches.

This does not fix the underlying memory-scaling issue (aiperf's
semianalysis_cc_traces_weka loader appears to reconstruct the full
949-trace corpus regardless of num-dataset-entries) -- it only reduces blast
radius. Tuning the real fix is left as a follow-up; see
design/throughput-test.md and session memory for the full investigation.

* Lower max concurrency in throughput sweep from 32 to 16

---------

Co-authored-by: Ngô Quang Hòa <hoanq3@vng.com.vn>
InferenceX-app's throughput-test ingest needs a GPU model to slot a sweep
point into their configs table (part of their uniqueness key), and neither
/discover, /version, nor either live-check artifact currently reports it.
The data exists in gpu_metrics_url's DCGM Prometheus feed (modelName label
per GPU), but querying it at ingest time would be wrong/missing for any
run where the pod has since moved, rescheduled, or been torn down --
ingest can lag a run by more than a few minutes, and definitely can for
backfill.

Add a shared utils/gpu_metrics.py helper that scrapes gpu_metrics_url once
at test time and bakes gpu_model into the artifact itself (same philosophy
as metadata.data already snapshotting /version verbatim). Wired into both
smoke-test (top-level gpu_model, alongside stack/run_type) and
throughput-test (data.gpu_model, alongside dataset/sweep). Raises loudly on
a pod reporting more than one distinct modelName across GPUs rather than
silently picking one; treated as best-effort enrichment otherwise (a lookup
failure logs a warning and sets gpu_model: null, it does not fail the
smoke/throughput check itself).

Co-authored-by: Ngô Quang Hòa <hoanq3@vng.com.vn>
Confirmed live (see session memory project_toolcalling_limitation.md):
at temperature=0.0, DeepSeek-Coder-V2-Lite-Instruct deterministically never
attempts a tool call under tool_choice=auto -- a stable decoding-time
property of a model that was never tool-call-instruction-tuned, not a
parser bug. The probe was testing model initiative, which no server-side
fix can change, instead of what we actually care about: whether the server
correctly parses and returns a well-formed call once the model is forced
to attempt one. Switch to tool_choice=required so the probe tests something
this repo can actually detect regressions in.

Co-authored-by: Ngô Quang Hòa <hoanq3@vng.com.vn>
InferenceX-app's second ask on the gpu-metrics thread: their configs
table's natural key also needs framework/precision/tp (and disaggregation
when applicable), and throughput-test/smoke-test are two fully independent
workflows with no shared run ID or guaranteed-same timestamp -- they can't
safely join them by (stack, latest-date) since a redeploy between the two
runs would silently attribute throughput numbers to the wrong config.

framework/precision/tp come straight off the matrix entry (already sourced
from /discover, no extra call). disaggregation comes from the /version
payload run() already fetches for redeploy detection (version_before) --
also no extra call, only included when present, mirroring metadata.data's
convention of only pd-disaggregation stacks reporting it.
Confirmed recurring on sglang-vanilla (hit on the very first main
validation run and twice more validating the config-snapshot feature): a
heavy sweep appears to make the stack's own /version endpoint transiently
unresponsive right after the sweep finishes. The redeploy-detection
re-check wasn't guarded, so this crashed the whole script and discarded an
already-completed sweep. Now best-effort: a failure here sets
redeployed_mid_run to None (unconfirmed, distinct from confirmed-false)
instead of losing the run.
Found via live investigation (curl reproduction + aiperf's own
error_summary export): the semianalysis_cc_traces_weka corpus's real
Claude-Code coding sessions routinely exceed 32K tokens, but this
deployment serves DeepSeek-Coder-V2-Lite-Instruct-FP8 with a 32768-token
max context (a serving-config choice -- the model itself supports up to
128K). Every oversized request was rejected outright with HTTP 400,
silently producing all-empty sweep-point stats no matter how
num-dataset-entries/conc-list/benchmark-duration-s were tuned -- none of
that tuning was ever going to fix a request-rejection problem.

Switch the utils/aiperf submodule to thangquang09/aiperf's
benchtool/agentx-weka branch, which adds --max-context-length filtering to
the weka loader (drops oversized conversations before they hit the
endpoint, mirrors WekaTraceLoader._filter_traces_by_max_context). Wire it
through aiperf_adapter.py and set MAX_CONTEXT_LENGTH=30000 in
run_throughput_test.py (30000, not 32768, to leave headroom for
completion tokens).

Also: turn the ad-hoc error_summary diagnostic used to find this into a
permanent, low-noise one (only logs when aiperf actually reports request
errors), and restore production config for sglang-vanilla (the tiny
single-conc/120s-duration values were diagnostic-only, not needed now
that the real cause is fixed).
Fixes the real root cause found via live investigation: the lightweight
concurrency-ping approach (plain --public-dataset sweep, no --scenario) was
a fundamental dataset/model-context mismatch, not something num-dataset-
entries/conc-list/benchmark-duration-s tuning could ever fix. Deployment
context has since been raised to 128K (external change), and this switches
to the same recipe benchmark-tmpl.yml/run-sweep.yml use for real agentic
coding workloads:

  --public-dataset semianalysis_cc_traces_weka_with_subagents_060826
  --scenario inferencex-agentx-mvp
  --benchmark-duration 600
  --use-server-token-count
  --num-dataset-entries 64
  --slice-duration 1.0
  --max-context-length 120000
  --unsafe-override

All new as code defaults in run_throughput_test.py, overridable per-stack
via throughput-tests.yaml (now much shorter -- every field has a sensible
default, stacks only need to declare conc-list).
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.

2 participants