Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ PRs are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for more details on the
For maintainer and agent instructions, start with [`AGENTS.md`](AGENTS.md). It links the configuration, workflow, eval, and recovery references.

To benchmark an existing server without CI or Slurm, see
[Run AgentX-Harness Standalone](inferencex-e2e/docs/agentx-standalone.md)
for client installation and a direct `agentx profile` command.
[Run AgentX with AIPerf Standalone](inferencex-e2e/docs/agentx-standalone.md)
for client installation and a direct `aiperf profile` command.

## Acknowledgements & Supporters
Thank you to Lisa Su and Anush Elangovan for providing the MI355X and CDNA3 GPUs for this free and open-source project. We want to recognize the many AMD contributors for their responsiveness and for debugging, optimizing, and validating performance across AMD GPUs.
Expand Down
4 changes: 2 additions & 2 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ SGLang、vLLM、TensorRT-LLM、CUDA、ROCm 等 AI 软件通过核函式優化、
维护者与 Agent 请从 [`AGENTS.md`](AGENTS.md) 开始阅读,其中链接了配置、Workflow、评估与恢复操作参考。

无需 CI 或 Slurm 即可对已有服务进行基准测试,请参阅
[独立运行 AgentX-Harness](inferencex-e2e/docs/agentx-standalone.md),
其中包含客户端安装步骤和直接执行的 `agentx profile` 命令。
[使用 AIPerf 独立运行 AgentX](inferencex-e2e/docs/agentx-standalone.md),
其中包含客户端安装步骤和直接执行的 `aiperf profile` 命令。

## 致谢与支持者
感谢 Lisa Su 与 Anush Elangovan 为这一免费开源项目提供 MI355X 与 CDNA3 GPU。我们也要感谢众多 AMD 贡献者的积极响应,以及他们在各类 AMD GPU 上进行调试、优化与性能验证所付出的努力。
Expand Down
40 changes: 27 additions & 13 deletions inferencex-e2e/docs/agentx-standalone.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
# Run AgentX-Harness Standalone
# Run AgentX with AIPerf Standalone

Run the AgentX client against an already-running OpenAI-compatible server.

## Install

Install the pinned PyPI release in a Python 3.11–3.13 virtual environment:
Install the pinned upstream AIPerf commit in a Python 3.11–3.13 virtual environment:

```bash
python3 -m venv .venv-agentx
source .venv-agentx/bin/activate
python -m pip install 'agentx-harness==1.0.12'
python -m pip install 'aiperf @ https://github.com/ai-dynamo/aiperf/archive/8aa7039f872cd029f05a4c313fb8759e9a482cd3.tar.gz'
```

The package version matches the `agentx-v1.0.12` GitHub release. The `agentx`
command selects the `agentx` scenario by default.
The commit matches `inferencex-e2e/infx/bench/agentic/requirements.txt`.

## Run

Expand All @@ -26,20 +25,32 @@ DATASET="semianalysis_cc_traces_weka_062126"
CONC=8
OUTPUT_DIR="$PWD/results/agentx-c${CONC}-$(date -u +%Y%m%dT%H%M%SZ)"

export AIPERF_DATASET_CONFIGURATION_TIMEOUT=1800
export AIPERF_SERVICE_PROFILE_CONFIGURE_TIMEOUT=1800
export AIPERF_UI_REALTIME_METRICS_ENABLED=true
export AIPERF_HTTP_TCP_USER_TIMEOUT=900000

mkdir -p "$OUTPUT_DIR"
agentx profile \
aiperf profile --scenario inferencex-agentx-mvp \
--url "$SERVER_URL" --endpoint /v1/chat/completions \
--model "$SERVED_MODEL_NAME" --tokenizer "$TOKENIZER" \
--tokenizer-trust-remote-code \
--concurrency "$CONC" \
--concurrency "$CONC" --benchmark-duration 3600 \
--endpoint-type chat --streaming --stats-interval 30 --random-seed 42 \
--trajectory-start-min-ratio 0.25 --trajectory-start-max-ratio 0.75 \
--trace-idle-gap-cap-seconds 300 --use-server-token-count --no-gpu-telemetry \
--num-dataset-entries 393 --slice-duration 1.0 \
--failed-request-threshold 0.10 --warmup-requests-per-lane 10 --warmup-grace-period 1800 \
--public-dataset "$DATASET" \
--output-artifact-dir "$OUTPUT_DIR/aiperf_artifacts" \
2>&1 | tee "$OUTPUT_DIR/aiperf.log"
```

The `agentx` preset supplies a one-hour profile, ten warmup requests per lane,
and shared reporting and runtime settings. Explicit CLI arguments and `AIPERF_*`
environment variables override preset defaults. Model, tokenizer, dataset,
The `inferencex-agentx-mvp` scenario locks the replay invariants: agentic replay
timing, streaming, `ignore_eos`, no input truncation, first-turn cache busting,
the accepted trace loaders and a 900-second minimum profile. The remaining flags
and `AIPERF_*` variables are the AgentX run settings InferenceX passes for every
point; keep them when reproducing a result. Model, tokenizer, dataset,
concurrency, and output remain caller inputs; `--tokenizer-trust-remote-code` is
needed only for tokenizers with custom code.

Expand All @@ -49,9 +60,9 @@ with `dsr1`, `qwen3.5`, or `qwen3.8next` explicitly select the
256k-capped corpus (`semianalysis_cc_traces_weka_062126_256k`). New model families
use the full corpus unless added to `CAPPED_FAMILIES` in
`infx/bench/agentic/traces.py`. `WEKA_LOADER_OVERRIDE` can pin either loader
regardless of model family; it does not configure the standalone `agentx` CLI.
regardless of model family; it does not configure the standalone `aiperf` CLI.

The default random seed is **42**. Keep this seed when reproducing AgentX results,
The random seed is **42**. Keep this seed when reproducing AgentX results,
or pass `--random-seed 123` to use a different seed.

Allow additional time for dataset preparation, warmup, and draining requests.
Expand All @@ -64,7 +75,7 @@ Add `--server-metrics "${SERVER_URL}/metrics"` if the server exposes metrics.
## Optional session-affinity flags

For DP-attention runs requiring router affinity, export these before
`agentx profile`. They add headers without replacing `X-Correlation-ID`.
`aiperf profile`. They add headers without replacing `X-Correlation-ID`.

```bash
# Adds X-Dynamo-Session-ID; subagents also get X-Dynamo-Parent-Session-ID.
Expand All @@ -73,3 +84,6 @@ export AIPERF_HTTP_X_DYNAMO_SESSION_ID_FROM_CORRELATION_ID=true
# Also sends X-Session-ID, if a front-end router needs it.
export AIPERF_HTTP_X_SESSION_ID_FROM_CORRELATION_ID=true
```

AIPerf sends `X-Session-Affinity` with the correlation ID by default; set
`AIPERF_HTTP_X_SESSION_AFFINITY_FROM_CORRELATION_ID=false` to omit it.
34 changes: 26 additions & 8 deletions inferencex-e2e/infx/bench/agentic/replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@
"AIPERF_USE_DYNAMO_CONV_AWARE_ROUTING",
)

SCENARIO = "inferencex-agentx-mvp"
# AIPerf's scenario locks invariants but leaves these to the caller.
PRESET_FLAGS = (
"--endpoint-type", "chat", "--streaming", "--stats-interval", "30", "--random-seed", "42",
"--trajectory-start-min-ratio", "0.25", "--trajectory-start-max-ratio", "0.75",
"--trace-idle-gap-cap-seconds", "300", "--use-server-token-count", "--no-gpu-telemetry",
"--num-dataset-entries", "393", "--slice-duration", "1.0",
) # fmt: skip
PRESET_ENVIRONMENT = {
"AIPERF_DATASET_CONFIGURATION_TIMEOUT": "1800",
"AIPERF_SERVICE_PROFILE_CONFIGURE_TIMEOUT": "1800",
"AIPERF_UI_REALTIME_METRICS_ENABLED": "true",
"AIPERF_HTTP_TCP_USER_TIMEOUT": "900000",
}

MIN_SCENARIO_DURATION_S = 900
FAST_DURATION_S = 1200
FAST_WARMUP_REQUESTS_PER_LANE = "1"
Expand Down Expand Up @@ -135,17 +150,20 @@ def _server_metrics_urls(env: Mapping[str, str]) -> tuple[str, ...]:
return tuple(parts)


def replay_environ(environ: Mapping[str, str]) -> dict[str, str]:
"""``environ`` over the preset's AIPerf runtime settings; the caller's values win."""
return {**PRESET_ENVIRONMENT, **environ}


def replay_argv(cfg: ReplayConfig, cli: str | Path) -> list[str]:
"""The ``agentx profile`` argv for ``cfg``; ``cli`` is the ``agentx`` executable."""
argv = [str(cli), "profile", "--url", cfg.url]
"""The ``aiperf profile`` argv for ``cfg``; ``cli`` is the ``aiperf`` executable."""
argv = [str(cli), "profile", "--scenario", SCENARIO, "--url", cfg.url]
argv += ["--model", cfg.model, "--tokenizer", cfg.tokenizer]
argv += ["--concurrency", str(cfg.concurrency), "--benchmark-duration", str(cfg.duration)]
if cfg.live_failed_request_threshold is not None:
argv += ["--failed-request-threshold", cfg.live_failed_request_threshold]
if cfg.warmup_requests_per_lane is not None:
argv += ["--warmup-requests-per-lane", cfg.warmup_requests_per_lane]
if cfg.warmup_grace_period is not None:
argv += ["--warmup-grace-period", cfg.warmup_grace_period]
argv += PRESET_FLAGS
argv += ["--failed-request-threshold", cfg.live_failed_request_threshold or "0.10"]
argv += ["--warmup-requests-per-lane", cfg.warmup_requests_per_lane or "10"]
argv += ["--warmup-grace-period", cfg.warmup_grace_period or "1800"]
if cfg.extra_inputs:
argv += ["--extra-inputs", *cfg.extra_inputs]
if cfg.dynamo_session_timeout is not None:
Expand Down
2 changes: 1 addition & 1 deletion inferencex-e2e/infx/bench/agentic/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
agentx-harness==1.0.12
aiperf @ https://github.com/ai-dynamo/aiperf/archive/8aa7039f872cd029f05a4c313fb8759e9a482cd3.tar.gz
datasets>=4.7.0
7 changes: 4 additions & 3 deletions inferencex-e2e/infx/bench/agentic/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
REQUIRED as REPLAY_REQUIRED,
ReplayConfig,
replay_argv,
replay_environ,
)
from infx.bench.agentic.venv import Runtime, bootstrap

Expand Down Expand Up @@ -133,11 +134,11 @@ def execute(plan: Plan, runtime: Runtime, environ: Mapping[str, str]) -> int:
rc = _open_power_window(plan, python, env)
if rc:
return rc
argv = replay_argv(cfg, runtime.agentx)
argv = replay_argv(cfg, runtime.aiperf)
(plan.result_dir / "benchmark_command.txt").write_text(f"{shlex.join(argv)}\n")
log = plan.result_dir / "benchmark.log"
with proc.DeferSignals() as signals:
replay_rc = 0 if signals.received else proc.tee(argv, log, env)
replay_rc = 0 if signals.received else proc.tee(argv, log, replay_environ(env))
if signals.received:
return 128 + signals.received
return _score(plan, python, env, replay_rc)
Expand Down Expand Up @@ -247,7 +248,7 @@ def main(argv: list[str]) -> int:
plan = Plan.from_env(os.environ)
runtime = Runtime.for_job(os.environ)
if not runtime.active():
print(f"Preparing the AgentX runtime in {runtime.venv}", flush=True)
print(f"Preparing the AIPerf runtime in {runtime.venv}", flush=True)
rc = bootstrap(runtime)
if rc:
return rc
Expand Down
12 changes: 6 additions & 6 deletions inferencex-e2e/infx/bench/agentic/venv.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""The isolated AgentX runtime: a per-job uv venv with the pinned PyPI release."""
"""The isolated AIPerf runtime: a per-job uv venv with the pinned upstream commit."""

from __future__ import annotations

Expand All @@ -20,7 +20,7 @@

@dataclass(frozen=True)
class Runtime:
"""Paths of one job's AgentX runtime directory."""
"""Paths of one job's AIPerf runtime directory."""

root: Path

Expand All @@ -42,8 +42,8 @@ def python(self) -> Path:
return self.venv / "bin" / "python"

@property
def agentx(self) -> Path:
return self.venv / "bin" / "agentx"
def aiperf(self) -> Path:
return self.venv / "bin" / "aiperf"

@property
def hf(self) -> Path:
Expand Down Expand Up @@ -77,8 +77,8 @@ def bootstrap(runtime: Runtime) -> int:
"benchmark client dependency bootstrap failed; inspect network/package "
"resolution before recipe repairs"
)
if not (_executable(runtime.agentx) and _executable(runtime.hf)):
raise BenchError(f"isolated AgentX environment is incomplete at {runtime.venv}")
if not (_executable(runtime.aiperf) and _executable(runtime.hf)):
raise BenchError(f"isolated AIPerf environment is incomplete at {runtime.venv}")
return 0


Expand Down
2 changes: 1 addition & 1 deletion inferencex-e2e/infx/datasets/proxy_to_weka.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Reads <in-dir>/<session_id>.jsonl produced by `sample_proxy_traces.py`
and writes <in-dir>/../<out-dir>/<session_id>.json in the v1 weka trace
format consumed by the kv-cache-tester replayer (see
agentx-harness: src/aiperf/dataset/loader/weka_trace_models.py).
aiperf: src/aiperf/dataset/loader/weka_trace_models.py).

Subagent grouping mirrors the conversation-view algorithm from the
SemiAnalysis claude-code-proxy:
Expand Down
11 changes: 6 additions & 5 deletions inferencex-e2e/infx/launch/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
AGENTIC = "agentic-coding"
_SHA = re.compile(r"[0-9a-f]{40}(?:[0-9a-f]{24})?")
_PINNED = re.compile(r"@(sha256:[0-9a-f]{64})$")
_HARNESS = re.compile(r"^agentx-harness==(\S+)", re.MULTILINE)
_AIPERF = re.compile(r"^aiperf @ \S+/archive/([0-9a-f]{40})\.tar\.gz$", re.MULTILINE)
_RESULT_GPUS = re.compile(r"_gpus_([0-9]+)")


Expand Down Expand Up @@ -71,6 +71,7 @@ class Versions(_Record):
srt_slurm_sha: str | None = None
srtctl_version: str | None = None
agentx_harness: str | None = None
aiperf_sha: str | None = None
image_digest: str | None = None


Expand Down Expand Up @@ -513,15 +514,15 @@ def _versions(env: Mapping[str, str], *, agentx: bool) -> Versions:
head = _git_head(repository_root())
workflow = _text(env, "GITHUB_SHA")
pinned = _PINNED.search(env.get("IMAGE", ""))
harness = None
aiperf = None
if agentx:
with contextlib.suppress(OSError):
match = _HARNESS.search(REQUIREMENTS.read_text())
harness = match.group(1) if match else None
match = _AIPERF.search(REQUIREMENTS.read_text())
aiperf = match.group(1) if match else None
return Versions(
inferencex_sha=head,
workflow_sha=workflow if workflow != head else None,
agentx_harness=harness,
aiperf_sha=aiperf,
image_digest=pinned.group(1) if pinned else None,
)

Expand Down
12 changes: 7 additions & 5 deletions inferencex-e2e/infx/tests/bench/test_agentic_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def _point(tmp_path: Path, **overrides: str | None) -> dict[str, str]:
def _runtime(tmp_path: Path) -> Runtime:
runtime = Runtime(tmp_path / "runtime")
runtime.python.parent.mkdir(parents=True)
stubs = {runtime.python: FAKE_PYTHON, runtime.agentx: FAKE_AIPERF, runtime.hf: FAKE_HF}
stubs = {runtime.python: FAKE_PYTHON, runtime.aiperf: FAKE_AIPERF, runtime.hf: FAKE_HF}
for path, text in stubs.items():
executable(path, text)
return runtime
Expand Down Expand Up @@ -350,6 +350,7 @@ def test_signal_during_the_replay_skips_scoring_and_exits_128_plus_n(tmp_path):
out = pathlib.Path(argv[argv.index("--output-artifact-dir") + 1])
out.mkdir(parents=True, exist_ok=True)
pathlib.Path(os.environ["REPLAY_ARGV"]).write_text(shlex.join([sys.argv[0], *argv]))
print("fake aiperf: configure timeout", os.environ.get("AIPERF_DATASET_CONFIGURATION_TIMEOUT"))
with open(os.environ["EVENTS"], "a") as events:
events.write("replay\n")
record = {
Expand Down Expand Up @@ -379,10 +380,10 @@ def test_signal_during_the_replay_skips_scoring_and_exits_128_plus_n(tmp_path):
venv) for target; do :; done; exec "$REAL_PYTHON" -m venv --without-pip "$target" ;;
pip)
bin="$(dirname "$4")"
printf '#!%s\n' "$bin/python" > "$bin/agentx"
cat "$FAKE_AIPERF_SCRIPT" >> "$bin/agentx"
printf '#!%s\n' "$bin/python" > "$bin/aiperf"
cat "$FAKE_AIPERF_SCRIPT" >> "$bin/aiperf"
printf '#!/bin/sh\necho "hf $*" >> "$EVENTS"\n' > "$bin/hf"
chmod +x "$bin/agentx" "$bin/hf" ;;
chmod +x "$bin/aiperf" "$bin/hf" ;;
esac
"""

Expand Down Expand Up @@ -433,8 +434,9 @@ def frontend(method: str, path: str) -> tuple[int, object]:
results = tmp_path / "results"
command = (results / "benchmark_command.txt").read_text()
assert command == (tmp_path / "replay-argv").read_text() + "\n"
assert shlex.split(command)[0] == str(tmp_path / "runtime/venv/bin/agentx")
assert shlex.split(command)[0] == str(tmp_path / "runtime/venv/bin/aiperf")
assert "fake aiperf: profiled 1 request" in (results / "benchmark.log").read_text()
assert "fake aiperf: configure timeout 1800" in result.stdout
assert "fake aiperf: profiled 1 request" in result.stdout
aggregate = json.loads((tmp_path / "agentx.json").read_text())
assert (aggregate["conc"], aggregate["num_requests_total"]) == (8, 1)
Loading
Loading