From 2ca177e04dea80b0016a104298955d6866cffc09 Mon Sep 17 00:00:00 2001 From: Cam Quilici Date: Wed, 14 Oct 2026 17:09:34 -0500 Subject: [PATCH 1/3] config: restore GB300 AgentX c5120 and c6656 points --- .../gb300-fp4/agentx/disagg-variants.yaml | 24 +++++++++++++++ inferencex-e2e/configs/nvidia-master.yaml | 30 +++++++++++++++++++ inferencex-e2e/perf-changelog.yaml | 9 ++++++ 3 files changed, 63 insertions(+) diff --git a/inferencex-e2e/benchmarks/multi_node/srt-slurm-recipes/dsv41flash/vllm/gb300-fp4/agentx/disagg-variants.yaml b/inferencex-e2e/benchmarks/multi_node/srt-slurm-recipes/dsv41flash/vllm/gb300-fp4/agentx/disagg-variants.yaml index d6cea5b751..68c0037113 100644 --- a/inferencex-e2e/benchmarks/multi_node/srt-slurm-recipes/dsv41flash/vllm/gb300-fp4/agentx/disagg-variants.yaml +++ b/inferencex-e2e/benchmarks/multi_node/srt-slurm-recipes/dsv41flash/vllm/gb300-fp4/agentx/disagg-variants.yaml @@ -203,3 +203,27 @@ override_4p1d_dep2_dep16_c1228: max-num-seqs: 18 max-num-batched-tokens: 180 compilation-config: '{"cudagraph_mode":"FULL_DECODE_ONLY","max_cudagraph_capture_size":108,"cudagraph_capture_sizes":[6,12,18,24,30,36,42,48,54,60,66,72,78,84,90,96,102,108]}' + +override_10p1d_dep2_dep16_c6656: + name: "dsv41flash-fp4-gb300-dynamo-vllm-agentx-disagg-10p1d-dep2-dep16-c6656" + roles: + prefill: + nodes: 5 + workers: 10 + decode: + args: + max-num-seqs: 132 + max-num-batched-tokens: 1320 + compilation-config: '{"cudagraph_mode":"FULL_DECODE_ONLY","max_cudagraph_capture_size":792,"cudagraph_capture_sizes":[6,12,18,24,30,36,42,48,54,60,66,72,78,84,90,96,102,108,114,120,126,132,138,144,150,156,162,168,174,180,186,192,198,204,210,216,222,228,234,240,246,252,258,264,270,276,282,288,294,300,306,312,318,324,330,336,342,348,354,360,366,372,378,384,390,396,402,408,414,420,426,432,438,444,450,456,462,468,474,480,486,492,498,504,510,516,522,528,534,540,546,552,558,564,570,576,582,588,594,600,606,612,618,624,630,636,642,648,654,660,666,672,678,684,690,696,702,708,714,720,726,732,738,744,750,756,762,768,774,780,786,792]}' + +override_8p1d_dep2_dep16_c5120: + name: "dsv41flash-fp4-gb300-dynamo-vllm-agentx-disagg-8p1d-dep2-dep16-c5120" + roles: + prefill: + nodes: 4 + workers: 8 + decode: + args: + max-num-seqs: 70 + max-num-batched-tokens: 700 + compilation-config: '{"cudagraph_mode":"FULL_DECODE_ONLY","max_cudagraph_capture_size":420,"cudagraph_capture_sizes":[6,12,18,24,30,36,42,48,54,60,66,72,78,84,90,96,102,108,114,120,126,132,138,144,150,156,162,168,174,180,186,192,198,204,210,216,222,228,234,240,246,252,258,264,270,276,282,288,294,300,306,312,318,324,330,336,342,348,354,360,366,372,378,384,390,396,402,408,414,420]}' diff --git a/inferencex-e2e/configs/nvidia-master.yaml b/inferencex-e2e/configs/nvidia-master.yaml index 4a4970d570..34df9d6a68 100644 --- a/inferencex-e2e/configs/nvidia-master.yaml +++ b/inferencex-e2e/configs/nvidia-master.yaml @@ -9408,6 +9408,36 @@ dsv41flash-fp4-gb300-dynamo-vllm-agentic-dspark-disagg: tp: 16 ep: 16 dp-attn: true + - spec-decoding: mtp + kv-offloading: dram + kv-offload-backend: { name: vllm-simple } + srt-recipe: disagg-variants.yaml:override_8p1d_dep2_dep16_c5120 + conc-list: [5120] + prefill: + num-worker: 8 + tp: 2 + ep: 2 + dp-attn: true + decode: + num-worker: 1 + tp: 16 + ep: 16 + dp-attn: true + - spec-decoding: mtp + kv-offloading: dram + kv-offload-backend: { name: vllm-simple } + srt-recipe: disagg-variants.yaml:override_10p1d_dep2_dep16_c6656 + conc-list: [6656] + prefill: + num-worker: 10 + tp: 2 + ep: 2 + dp-attn: true + decode: + num-worker: 1 + tp: 16 + ep: 16 + dp-attn: true # One serving GPU, stock ARM64 release, native AgentX client and proportional host cache. qwen3.8next-fp4-gb300-sglang-agentic-mtp: diff --git a/inferencex-e2e/perf-changelog.yaml b/inferencex-e2e/perf-changelog.yaml index 7f8d616d8b..13322d8097 100644 --- a/inferencex-e2e/perf-changelog.yaml +++ b/inferencex-e2e/perf-changelog.yaml @@ -9823,3 +9823,12 @@ - "Use master-config recipe selectors and composed AgentX fragments; preserve the dedicated frontend/client node and the c2884 and c1228 tuning." - "Backport the vLLM Python sidecar reasoning-parser fix through the shared patch mechanism while retaining the main srt-slurm pin." pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/3941 + +- config-keys: + - dsv41flash-fp4-gb300-dynamo-vllm-agentic-dspark-disagg + scenario-type: + - agentic-coding + description: + - "Append the GB300 DeepSeek-V4.1-Flash Dynamo + vLLM AgentX 8P1D c5120 and 10P1D c6656 points to the existing curve." + pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/4074 + append-only: true From 56a55dad0e0f70ee1d7373f3e40267f76766cf6d Mon Sep 17 00:00:00 2001 From: Cam Quilici Date: Wed, 14 Oct 2026 17:24:37 -0500 Subject: [PATCH 2/3] fix: bound AgentX memory for new GB300 high-concurrency points --- .../gb300-fp4/agentx/disagg-variants.yaml | 8 + .../patches/52-spawn-worker-pinning.patch | 183 ++++++++++++++++++ .../infx/bench/agentic/patches/README.md | 7 + inferencex-e2e/infx/bench/agentic/venv.py | 12 ++ .../infx/tests/bench/test_agentic_runtime.py | 22 ++- inferencex-e2e/perf-changelog.yaml | 1 + 6 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 inferencex-e2e/infx/bench/agentic/patches/52-spawn-worker-pinning.patch create mode 100644 inferencex-e2e/infx/bench/agentic/patches/README.md diff --git a/inferencex-e2e/benchmarks/multi_node/srt-slurm-recipes/dsv41flash/vllm/gb300-fp4/agentx/disagg-variants.yaml b/inferencex-e2e/benchmarks/multi_node/srt-slurm-recipes/dsv41flash/vllm/gb300-fp4/agentx/disagg-variants.yaml index 68c0037113..ccbb6c20ec 100644 --- a/inferencex-e2e/benchmarks/multi_node/srt-slurm-recipes/dsv41flash/vllm/gb300-fp4/agentx/disagg-variants.yaml +++ b/inferencex-e2e/benchmarks/multi_node/srt-slurm-recipes/dsv41flash/vllm/gb300-fp4/agentx/disagg-variants.yaml @@ -206,6 +206,10 @@ override_4p1d_dep2_dep16_c1228: override_10p1d_dep2_dep16_c6656: name: "dsv41flash-fp4-gb300-dynamo-vllm-agentx-disagg-10p1d-dep2-dep16-c6656" + benchmark: + env: + AIPERF_RUNTIME_PATCH: /infmax-workspace/infx/bench/agentic/patches/52-spawn-worker-pinning.patch + AIPERF_METRICS_LIST_BACKEND: tdigest roles: prefill: nodes: 5 @@ -218,6 +222,10 @@ override_10p1d_dep2_dep16_c6656: override_8p1d_dep2_dep16_c5120: name: "dsv41flash-fp4-gb300-dynamo-vllm-agentx-disagg-8p1d-dep2-dep16-c5120" + benchmark: + env: + AIPERF_RUNTIME_PATCH: /infmax-workspace/infx/bench/agentic/patches/52-spawn-worker-pinning.patch + AIPERF_METRICS_LIST_BACKEND: tdigest roles: prefill: nodes: 4 diff --git a/inferencex-e2e/infx/bench/agentic/patches/52-spawn-worker-pinning.patch b/inferencex-e2e/infx/bench/agentic/patches/52-spawn-worker-pinning.patch new file mode 100644 index 0000000000..ad9937f9f3 --- /dev/null +++ b/inferencex-e2e/infx/bench/agentic/patches/52-spawn-worker-pinning.patch @@ -0,0 +1,183 @@ +diff --git a/src/aiperf/credit/sticky_router.py b/src/aiperf/credit/sticky_router.py +index 973c024be1..b67df8674f 100644 +--- a/src/aiperf/credit/sticky_router.py ++++ b/src/aiperf/credit/sticky_router.py +@@ -22,7 +22,7 @@ + from dataclasses import dataclass, field + from typing import TYPE_CHECKING, Protocol, runtime_checkable + +-from aiperf.common.enums import CommAddress ++from aiperf.common.enums import CommAddress, ConversationBranchMode + from aiperf.common.mixins import CommunicationMixin + from aiperf.common.protocols import ( + StreamingPullClientProtocol, +@@ -49,9 +49,9 @@ + + @dataclass(slots=True) + class _StickyEntry: +- """Sticky-routing state for a root correlation id. ++ """Sticky-routing state for a root or SPAWN-child correlation id. + +- Tracks which worker owns the session and a refcount so DAG children that ++ Tracks which worker owns the session and a refcount so FORK children that + pin themselves to the parent's worker can keep the entry alive past the + parent's own final turn. ``parent_final_seen`` records whether the owning + session has finished its final turn; the entry is popped only once both +@@ -75,8 +75,10 @@ class WorkerLoad: + + Note on active_sessions: + - active_sessions and active_session_ids only represent the number of sticky sessions assigned +- to the worker, which inherently means that it only tracks sessions with MORE turns left. This is +- because sticky sessions are only created when more than 1 turn exists, and are removed when SENDING the final turn. ++ to the worker. An entry is held from the session's first non-final turn (or a final turn that ++ forks) until it is released: when SENDING the final turn, or, for a forking final turn, when its ++ FORK children release it. ++ - Roots and SPAWN children each count as a session; FORK children share their parent's. + """ + + worker_id: str +@@ -189,6 +191,9 @@ class StickyCreditRouter(CommunicationMixin): + - First turn → least-loaded worker (creates sticky session). + - Subsequent turns → same worker via sticky session lookup. + - Final turn → cleanup sticky session. ++ - FORK children → parent's worker via the parent's refcounted entry. ++ - SPAWN children → parent's worker if its entry is live (else least-loaded), ++ then pinned under their own id until their final turn. + + Load Balancing: + - Least-loaded worker selection for new sessions using fair load balancing +@@ -282,11 +287,11 @@ def __init__( + Callable[[FirstToken], Awaitable[None]] | None + ) = None + +- # Sticky sessions: routing_key -> _StickyEntry +- # Routes all turns of a conversation (and DAG children pinned to it) to the +- # same worker. Required because workers cache UserSession state by +- # x_correlation_id. The routing key is ``parent_correlation_id or +- # x_correlation_id`` so FORK-mode children co-locate with their parent. ++ # Sticky sessions: owner correlation id -> _StickyEntry ++ # Routes all turns of a session (and FORK children pinned to it) to the ++ # same worker, because workers cache UserSession state by ++ # x_correlation_id. Entries are keyed by the owning root's or SPAWN ++ # child's own id; FORK children route via their parent's key. + self._sticky_sessions: dict[str, _StickyEntry] = {} + + self._cancellation_pending: bool = False +@@ -361,9 +366,25 @@ async def send_credit(self, credit: Credit) -> None: + if not credit.x_correlation_id: + raise RuntimeError("x_correlation_id must be set in Credit") + +- # DAG children pin to their parent's worker; otherwise pin to self. +- routing_key = credit.parent_correlation_id or credit.x_correlation_id +- sticky_entry = self._sticky_sessions.get(routing_key) ++ # A SPAWN child pins under its own id from its first turn, so it keeps ++ # one worker even when the parent has no entry (parent finished, or ++ # gated or rootless at replay start). FORK children route via the ++ # parent's entry; roots pin to self. ++ is_final = credit.is_final_turn ++ is_dag_child = credit.parent_correlation_id is not None ++ # Identity check: CaseInsensitiveStrEnum.__eq__ is costly on this hot ++ # path, and every Credit is built with enum members. ++ is_spawn_child = ( ++ is_dag_child and credit.branch_mode is ConversationBranchMode.SPAWN ++ ) ++ own_key = credit.x_correlation_id ++ own_entry = self._sticky_sessions.get(own_key) if is_spawn_child else None ++ if own_entry is not None: ++ routing_key = own_key ++ sticky_entry = own_entry ++ else: ++ routing_key = credit.parent_correlation_id or own_key ++ sticky_entry = self._sticky_sessions.get(routing_key) + sticky_worker_id = sticky_entry.worker_id if sticky_entry is not None else None + + # Use existing sticky session if worker still valid +@@ -404,23 +425,15 @@ async def send_credit(self, credit: Credit) -> None: + worker_id = best_worker_id + + # Create or rebind the sticky entry for non-final turns; also create +- # it when the final turn declares DAG spawns so the orchestrator's ++ # it when the final turn declares FORK children so the orchestrator's + # register_child_routing can find it. + # +- # DAG branch-children (parent_correlation_id set — FORK or SPAWN) +- # must not auto-create when the parent's sticky entry is already +- # gone. The auto-create path would mint a fresh entry keyed by the +- # parent's id, bumping ``active_sessions`` with no path to evict it +- # (final-turn eviction is gated on parent_correlation_id is None; +- # release_child_routing only decrements an existing entry). That +- # leaks active_sessions and biases load balancing. When the parent +- # entry still exists, both FORK and SPAWN children co-locate via +- # the sticky hit above (routing_key = parent_correlation_id). +- # SPAWN differs only in refcount: the orchestrator does not call +- # register_child_routing for SPAWN. When the parent entry is gone, +- # children fall through to least-loaded without minting a leak. +- is_dag_child = credit.parent_correlation_id is not None +- if not credit.is_final_turn or credit.has_forks: ++ # DAG children never mint an entry keyed by the parent's id: nothing ++ # would release it (final-turn eviction is keyed by the owner's own ++ # id; release_child_routing only decrements an existing entry), so ++ # it would leak active_sessions and bias load balancing. SPAWN ++ # children get their own entry below instead. ++ if not is_final or credit.has_forks: + if sticky_entry is None and not is_dag_child: + sticky_entry = _StickyEntry(worker_id=worker_id) + self._sticky_sessions[routing_key] = sticky_entry +@@ -436,20 +449,29 @@ async def send_credit(self, credit: Credit) -> None: + load.active_sessions += 1 + load.active_session_ids.add(routing_key) + +- # Owning session's final turn: mark parent_final_seen and decrement the +- # reservation. DAG children never touch the parent entry (managed via +- # release_child_routing). If this turn has DAG spawns, leave the entry ++ if is_spawn_child and own_entry is None and (not is_final or credit.has_forks): ++ own_entry = self._sticky_sessions[own_key] = _StickyEntry( ++ worker_id=worker_id ++ ) ++ load = self._workers[worker_id] ++ load.active_sessions += 1 ++ load.active_session_ids.add(own_key) ++ ++ # Owning session's final turn (a root or SPAWN child, keyed by its own ++ # id): mark parent_final_seen and decrement the reservation. FORK ++ # children never touch the parent entry (managed via ++ # release_child_routing). If this turn forks, leave the entry + # in place so register_child_routing lands on the same _StickyEntry. +- if credit.is_final_turn and credit.parent_correlation_id is None: +- entry = sticky_entry or self._sticky_sessions.get(routing_key) ++ if is_final and (not is_dag_child or is_spawn_child): ++ entry = own_entry if is_spawn_child else sticky_entry + if entry is not None: + entry.parent_final_seen = True + entry.ref_count -= 1 + if entry.ref_count <= 0 and not credit.has_forks: +- self._sticky_sessions.pop(routing_key, None) ++ self._sticky_sessions.pop(own_key, None) + load = self._workers[worker_id] + load.active_sessions -= 1 +- load.active_session_ids.discard(routing_key) ++ load.active_session_ids.discard(own_key) + + self._track_credit_sent(worker_id, credit.id) + +@@ -499,9 +521,9 @@ def mark_credits_complete(self) -> None: + def register_child_routing(self, parent_correlation_id: str) -> None: + """Increment the sticky-routing refcount for a parent's entry. + +- Called by ``BranchOrchestrator`` before dispatching each DAG child so ++ Called by ``BranchOrchestrator`` before dispatching each FORK child so + the parent's sticky entry survives past its own final turn until every +- descendant child session has terminated. If the parent has no active ++ FORK child has terminated. If the parent has no active + sticky entry we log a warning and continue without raising - the + child will route via least-loaded selection rather than co-locating + with the parent's worker, losing prefix-cache locality but not +@@ -518,7 +540,7 @@ def register_child_routing(self, parent_correlation_id: str) -> None: + ) + + def release_child_routing(self, parent_correlation_id: str) -> None: +- """Decrement the sticky-routing refcount when a DAG child terminates. ++ """Decrement the sticky-routing refcount when a FORK child terminates. + + Called by ``BranchOrchestrator`` when a child session reaches a leaf + or errors out. If the refcount reaches zero and the parent's own final diff --git a/inferencex-e2e/infx/bench/agentic/patches/README.md b/inferencex-e2e/infx/bench/agentic/patches/README.md new file mode 100644 index 0000000000..c1e30279e7 --- /dev/null +++ b/inferencex-e2e/infx/bench/agentic/patches/README.md @@ -0,0 +1,7 @@ +# AgentX runtime patches + +Recipes opt into a patch using `benchmark.env.AIPERF_RUNTIME_PATCH`. The AgentX bootstrap applies it to the isolated PyPI installation before starting the client. Source paths use the upstream `src/aiperf` layout (`git apply -p2`). Unselected recipes use the unmodified release. + +| Patch | Source | Fix | +| --- | --- | --- | +| `52-spawn-worker-pinning.patch` | [agentx-harness #52](https://github.com/SemiAnalysisAI/agentx-harness/pull/52), commit `b5d7652b50aceb9e0c4ca77daf9ae8f472ac77fd` | Pin each SPAWN child to one client worker to prevent stale session copies accumulating across workers. Includes the runtime routing change; documentation-only changes and upstream tests are omitted. | diff --git a/inferencex-e2e/infx/bench/agentic/venv.py b/inferencex-e2e/infx/bench/agentic/venv.py index 779df64348..e93448f37a 100644 --- a/inferencex-e2e/infx/bench/agentic/venv.py +++ b/inferencex-e2e/infx/bench/agentic/venv.py @@ -2,6 +2,7 @@ from __future__ import annotations +import hashlib import os import shutil import sys @@ -79,8 +80,19 @@ def bootstrap(runtime: Runtime) -> int: ) if not (_executable(runtime.agentx) and _executable(runtime.hf)): raise BenchError(f"isolated AgentX environment is incomplete at {runtime.venv}") + if patch := os.environ.get("AIPERF_RUNTIME_PATCH"): + apply_patch(runtime, Path(patch)) return 0 +def apply_patch(runtime: Runtime, patch: Path) -> None: + """Apply a recipe-selected source patch to the isolated installed harness.""" + digest = hashlib.sha256(patch.read_bytes()).hexdigest() + site_packages = runtime.venv / "lib" / f"python{PYTHON_VERSION}" / "site-packages" + print(f"Applying AgentX runtime patch {patch.name} sha256={digest}", flush=True) + if proc.call(["git", "apply", "-p2", str(patch.resolve())], cwd=site_packages): + raise BenchError(f"AgentX runtime patch failed: {patch}") + + def _executable(path: Path) -> bool: return path.is_file() and os.access(path, os.X_OK) diff --git a/inferencex-e2e/infx/tests/bench/test_agentic_runtime.py b/inferencex-e2e/infx/tests/bench/test_agentic_runtime.py index 8237b0ebab..197c9f08a9 100644 --- a/inferencex-e2e/infx/tests/bench/test_agentic_runtime.py +++ b/inferencex-e2e/infx/tests/bench/test_agentic_runtime.py @@ -10,7 +10,7 @@ import pytest -from infx.bench.agentic.venv import Runtime, bootstrap +from infx.bench.agentic.venv import Runtime, apply_patch, bootstrap from infx.bench.env import BenchError from infx.tests.bench.stubs import executable @@ -123,3 +123,23 @@ def test_unusable_runtime_fails_with_the_reason(tmp_path, tools, monkeypatch, fa with pytest.raises(BenchError, match=message): bootstrap(Runtime(tmp_path / "runtime")) + + +def test_runtime_patch_changes_installed_code_and_rejects_mismatch(tmp_path): + runtime = Runtime(tmp_path / "runtime") + site = runtime.venv / "lib" / "python3.11" / "site-packages" + site.mkdir(parents=True) + installed = site / "example.py" + installed.write_text("value = 1\n") + patch = tmp_path / "example.patch" + patch.write_text( + "diff --git a/src/example.py b/src/example.py\n" + "--- a/src/example.py\n+++ b/src/example.py\n" + "@@ -1 +1 @@\n-value = 1\n+value = 2\n" + ) + + apply_patch(runtime, patch) + + assert installed.read_text() == "value = 2\n" + with pytest.raises(BenchError, match="runtime patch failed"): + apply_patch(runtime, patch) diff --git a/inferencex-e2e/perf-changelog.yaml b/inferencex-e2e/perf-changelog.yaml index 13322d8097..714246d645 100644 --- a/inferencex-e2e/perf-changelog.yaml +++ b/inferencex-e2e/perf-changelog.yaml @@ -9830,5 +9830,6 @@ - agentic-coding description: - "Append the GB300 DeepSeek-V4.1-Flash Dynamo + vLLM AgentX 8P1D c5120 and 10P1D c6656 points to the existing curve." + - "Use AgentX 1.0.9 with the SPAWN worker-pinning patch and t-digest timing storage only for these new high-concurrency points." pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/4074 append-only: true From 89d7321e1619449418b7fe0b230e43e1e63152d5 Mon Sep 17 00:00:00 2001 From: Cam Quilici Date: Wed, 14 Oct 2026 19:44:05 -0500 Subject: [PATCH 3/3] fix: apply AgentX runtime patches without git --- inferencex-e2e/infx/bench/agentic/patches/README.md | 2 +- inferencex-e2e/infx/bench/agentic/venv.py | 5 ++++- inferencex-e2e/infx/tests/bench/test_agentic_runtime.py | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/inferencex-e2e/infx/bench/agentic/patches/README.md b/inferencex-e2e/infx/bench/agentic/patches/README.md index c1e30279e7..499cb27c00 100644 --- a/inferencex-e2e/infx/bench/agentic/patches/README.md +++ b/inferencex-e2e/infx/bench/agentic/patches/README.md @@ -1,6 +1,6 @@ # AgentX runtime patches -Recipes opt into a patch using `benchmark.env.AIPERF_RUNTIME_PATCH`. The AgentX bootstrap applies it to the isolated PyPI installation before starting the client. Source paths use the upstream `src/aiperf` layout (`git apply -p2`). Unselected recipes use the unmodified release. +Recipes opt into a patch using `benchmark.env.AIPERF_RUNTIME_PATCH`. The AgentX bootstrap applies it to the isolated PyPI installation before starting the client. Source paths use the upstream `src/aiperf` layout (`patch -p2`). Unselected recipes use the unmodified release. | Patch | Source | Fix | | --- | --- | --- | diff --git a/inferencex-e2e/infx/bench/agentic/venv.py b/inferencex-e2e/infx/bench/agentic/venv.py index e93448f37a..6bfe8ba841 100644 --- a/inferencex-e2e/infx/bench/agentic/venv.py +++ b/inferencex-e2e/infx/bench/agentic/venv.py @@ -90,7 +90,10 @@ def apply_patch(runtime: Runtime, patch: Path) -> None: digest = hashlib.sha256(patch.read_bytes()).hexdigest() site_packages = runtime.venv / "lib" / f"python{PYTHON_VERSION}" / "site-packages" print(f"Applying AgentX runtime patch {patch.name} sha256={digest}", flush=True) - if proc.call(["git", "apply", "-p2", str(patch.resolve())], cwd=site_packages): + if proc.call( + ["patch", "--batch", "--forward", "--fuzz=0", "-p2", "-i", str(patch.resolve())], + cwd=site_packages, + ): raise BenchError(f"AgentX runtime patch failed: {patch}") diff --git a/inferencex-e2e/infx/tests/bench/test_agentic_runtime.py b/inferencex-e2e/infx/tests/bench/test_agentic_runtime.py index 197c9f08a9..92cd62a9a9 100644 --- a/inferencex-e2e/infx/tests/bench/test_agentic_runtime.py +++ b/inferencex-e2e/infx/tests/bench/test_agentic_runtime.py @@ -125,7 +125,7 @@ def test_unusable_runtime_fails_with_the_reason(tmp_path, tools, monkeypatch, fa bootstrap(Runtime(tmp_path / "runtime")) -def test_runtime_patch_changes_installed_code_and_rejects_mismatch(tmp_path): +def test_runtime_patch_changes_installed_code_and_rejects_mismatch(tmp_path, tools): runtime = Runtime(tmp_path / "runtime") site = runtime.venv / "lib" / "python3.11" / "site-packages" site.mkdir(parents=True) @@ -141,5 +141,6 @@ def test_runtime_patch_changes_installed_code_and_rejects_mismatch(tmp_path): apply_patch(runtime, patch) assert installed.read_text() == "value = 2\n" + assert not (tmp_path / "forbidden").exists() with pytest.raises(BenchError, match="runtime patch failed"): apply_patch(runtime, patch)