diff --git a/.dockerignore b/.dockerignore index 01e4e3b16af9..aba29c1ec9ac 100644 --- a/.dockerignore +++ b/.dockerignore @@ -41,9 +41,8 @@ **/*safetensors container/Dockerfile* container/**/*.Dockerfile -# The AISimulate dependency contract test reads this template from runtime test -# images; keep every other generated/template Dockerfile excluded. -!container/templates/wheel_builder.Dockerfile +# Dependency contract tests inspect the source installation templates in test images. +!container/templates/*.Dockerfile container/render.py container/context.yaml # container/compliance/ is now COPY'd into each image's `licenses` Dockerfile diff --git a/Cargo.lock b/Cargo.lock index 7ee55365e44b..ce79693ad9f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,10 +33,9 @@ dependencies = [ ] [[package]] -name = "aisimulate-core" -version = "0.13.0-dev.202610290000000087" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb823782e806dd74cba719906a6b828b86f415aad54c6d5016c29ff278e89319" +name = "aisim-core" +version = "0.14.0" +source = "git+https://github.com/ai-dynamo/aisimulate.git?rev=74dcdfcde184070d39e813ba24424b32e9627f00#74dcdfcde184070d39e813ba24424b32e9627f00" dependencies = [ "anyhow", "bincode 1.3.3", @@ -2855,7 +2854,7 @@ dependencies = [ name = "dynamo-mocker" version = "1.7.0" dependencies = [ - "aisimulate-core", + "aisim-core", "anyhow", "blake3", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 84e47e9e5a2e..5205cd1ec4bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,7 +65,7 @@ keywords = ["llm", "genai", "inference", "nvidia", "distributed"] dynamo-router-policies = { path = "lib/router-plugins/builtin", version = "1.7.0" } dynamo-generate-format-catalog = { path = "lib/generate-format-catalog", version = "1.7.0" } # Published separately by https://github.com/ai-dynamo/aisimulate. -aisimulate-core = { version = "=0.13.0-dev.202610290000000087" } +aisimulate-core = { package = "aisim-core", version = "=0.14.0" } # Local crates dynamo-runtime = { path = "lib/runtime", version = "1.7.0" } @@ -237,3 +237,7 @@ lto = "thin" inherits = "release" debug = true strip = false + +# Release-only AISim 0.14.0 RC0 source pin for Dynamo 1.7.0. +[patch.crates-io] +aisim-core = { git = "https://github.com/ai-dynamo/aisimulate.git", rev = "74dcdfcde184070d39e813ba24424b32e9627f00" } diff --git a/benchmarks/pyproject.toml b/benchmarks/pyproject.toml index 5760b9f24751..d6dcc7f8459f 100644 --- a/benchmarks/pyproject.toml +++ b/benchmarks/pyproject.toml @@ -39,7 +39,7 @@ classifiers = [ ] dependencies = [ - "aisimulate==0.13.0.dev202610290000000087 ; python_version >= '3.11' and python_version < '3.14'", + "aisim==0.14.0 ; python_version >= '3.11' and python_version < '3.14'", "aiperf==0.13.0", "matplotlib", "networkx", diff --git a/components/src/dynamo/aisimulate/tests/dgd/e2e/configs/pareto.yaml b/components/src/dynamo/aisimulate/tests/dgd/e2e/configs/pareto.yaml index e73fdeb23d46..32d42c78e100 100644 --- a/components/src/dynamo/aisimulate/tests/dgd/e2e/configs/pareto.yaml +++ b/components/src/dynamo/aisimulate/tests/dgd/e2e/configs/pareto.yaml @@ -16,7 +16,7 @@ engine: parallelism: preset: - {replicas: 1, tensor: 1, pipeline: 1, attention_data: 1, moe_tensor: 1, moe_expert: 1} - scheduler: {max_batched_tokens: 32, max_sequences: {choices: [1, 4]}} + scheduler: {max_batched_tokens: 64, max_sequences: {choices: [1, 4]}} kv_cache: {block_size: 64, prefix_caching: true, capacity: {type: fixed, blocks: 256}} timing: {type: fixed, prefill_ms: 1, decode_ms: 1} optimization: diff --git a/components/src/dynamo/planner/tests/integration/test_fpm_event_delivery.py b/components/src/dynamo/planner/tests/integration/test_fpm_event_delivery.py index 699f256f2ce5..d84b96e3f12b 100644 --- a/components/src/dynamo/planner/tests/integration/test_fpm_event_delivery.py +++ b/components/src/dynamo/planner/tests/integration/test_fpm_event_delivery.py @@ -73,7 +73,11 @@ def test_endpoint_fpm_reaches_planner_and_drives_scale_up(tmp_path): subprocess.run( [sys.executable, __file__], check=True, - env={**os.environ, "DYN_FILE_KV": str(tmp_path)}, + env={ + **os.environ, + "DYN_FILE_KV": str(tmp_path), + "DYN_EVENT_PLANE_HOST": "127.0.0.1", + }, timeout=30, ) diff --git a/components/src/dynamo/planner/tests/integration/test_virtual_connector.py b/components/src/dynamo/planner/tests/integration/test_virtual_connector.py index 4db61b84d7a1..7e0ae990f239 100644 --- a/components/src/dynamo/planner/tests/integration/test_virtual_connector.py +++ b/components/src/dynamo/planner/tests/integration/test_virtual_connector.py @@ -202,7 +202,7 @@ async def async_internal(distributed_runtime): await client.complete(event) -@pytest.mark.timeout(15) +@pytest.mark.timeout(50) def test_wait_for_unacknowledged_decision(tmp_path): # DistributedRuntime is process-global, so keep it out of the pytest worker. with _isolated_etcd(tmp_path / "etcd") as etcd_port: diff --git a/components/src/dynamo/planner/tests/unit/test_virtual_connector.py b/components/src/dynamo/planner/tests/unit/test_virtual_connector_unit.py similarity index 100% rename from components/src/dynamo/planner/tests/unit/test_virtual_connector.py rename to components/src/dynamo/planner/tests/unit/test_virtual_connector_unit.py diff --git a/components/src/dynamo/profiler/tests/test_replay_optimize.py b/components/src/dynamo/profiler/tests/test_replay_optimize.py index 9d302b67d42d..0ed588a3a281 100644 --- a/components/src/dynamo/profiler/tests/test_replay_optimize.py +++ b/components/src/dynamo/profiler/tests/test_replay_optimize.py @@ -1330,6 +1330,7 @@ def test_agg_optimizer_synthetic_replay_smoke(monkeypatch) -> None: ), router_mode="both", overlap_credits=[0.0, 1.0], + prefill_load_scales=[1.0], ) ) @@ -1357,6 +1358,7 @@ def test_agg_optimizer_timed_trace_smoke(tmp_path, monkeypatch) -> None: ), router_mode="both", overlap_credits=[0.0, 1.0], + prefill_load_scales=[1.0], ) ) @@ -1383,6 +1385,7 @@ def test_optimizer_synthetic_replay_smoke(tmp_path, monkeypatch) -> None: mean_e2e_latency_ms=100000.0, ), overlap_credits=[0.0, 1.0], + prefill_load_scales=[1.0], ) ) @@ -1409,6 +1412,7 @@ def test_optimizer_timed_trace_smoke(tmp_path, monkeypatch) -> None: mean_e2e_latency_ms=100000.0, ), overlap_credits=[0.0, 1.0], + prefill_load_scales=[1.0], ) ) diff --git a/container/Dockerfile.test b/container/Dockerfile.test index c069035dff19..1a2c628e5443 100644 --- a/container/Dockerfile.test +++ b/container/Dockerfile.test @@ -68,6 +68,8 @@ COPY --chmod=755 --chown=dynamo:0 \ .github/scripts/helm_chart_version.py \ .github/scripts/retry_kubectl.sh \ .github/scripts/collect_vcluster_diagnostics.sh /workspace/.github/scripts/ +# Dependency contract tests inspect every runtime installation template. +COPY --chmod=775 --chown=dynamo:0 container/templates/ /workspace/container/templates/ # Required: blocks dynamo.vllm/sglang from shadowing the installed packages. COPY --chmod=664 --chown=dynamo:0 conftest.py /workspace/conftest.py COPY --chmod=775 --chown=dynamo:0 benchmarks/ /workspace/benchmarks/ diff --git a/container/deps/README.md b/container/deps/README.md index 5cff3724a4f6..e5f82a35ae78 100644 --- a/container/deps/README.md +++ b/container/deps/README.md @@ -8,7 +8,7 @@ so each image only installs what it needs. | File | Purpose | |------|---------| | `requirements.common.txt` | Core deps shared by all containers | -| `requirements.aisimulate.txt` | Published AISimulate wheel staged into wheelhouses | +| `requirements.aisimulate.txt` | Source-built AISim wheel staged into release wheelhouses | | `requirements.planner.txt` | Planner, profiler, and global_planner deps | | `requirements.frontend.txt` | Frontend deps | | `requirements.benchmark.txt` | Benchmark and profiling tools | diff --git a/container/deps/build_aisimulate.py b/container/deps/build_aisimulate.py new file mode 100644 index 000000000000..67a52b1bea77 --- /dev/null +++ b/container/deps/build_aisimulate.py @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Build the AISimulate wheel from Dynamo's exact Rust source patch.""" + +import argparse +import re +import subprocess +import sys +from pathlib import Path + +from packaging.requirements import Requirement + +try: + import tomllib +except ModuleNotFoundError: # Python 3.10 + import tomli as tomllib + + +def build(root: Path, source: Path, output: Path) -> None: + with (root / "Cargo.toml").open("rb") as handle: + patch = tomllib.load(handle)["patch"]["crates-io"]["aisim-core"] + repository, revision = patch["git"], patch["rev"] + if repository != "https://github.com/ai-dynamo/aisimulate.git" or not re.fullmatch( + r"[0-9a-f]{40}", revision + ): + raise ValueError( + "AISimulate source patch must use the canonical repository and a full SHA" + ) + + source.mkdir(parents=True, exist_ok=False) + subprocess.run(["git", "init", str(source)], check=True) + subprocess.run( + ["git", "-C", str(source), "fetch", "--depth=1", patch["git"], patch["rev"]], + check=True, + ) + subprocess.run( + ["git", "-C", str(source), "checkout", "--detach", "FETCH_HEAD"], check=True + ) + project = source / "python/aisimulate" + with (project / "pyproject.toml").open("rb") as handle: + project_metadata = tomllib.load(handle)["project"] + if project_metadata["name"] != "aisim": + raise ValueError("RC source must publish the aisim distribution") + version = project_metadata["version"] + with (root / "pyproject.toml").open("rb") as handle: + requirements = tomllib.load(handle)["project"]["dependencies"] + requirement = next( + Requirement(item) for item in requirements if Requirement(item).name == "aisim" + ) + if requirement.url is not None or not requirement.specifier.contains(version): + raise ValueError( + f"Source wheel version {version} does not satisfy {requirement}" + ) + print(f"Building AISimulate {version} from {patch['rev']}", flush=True) + subprocess.run( + [ + "maturin", + "build", + "--release", + "--locked", + "--interpreter", + sys.executable, + "--out", + str(output.resolve()), + ], + cwd=project, + check=True, + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, required=True) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + build(args.root, args.source, args.output) diff --git a/container/deps/requirements.aisimulate.txt b/container/deps/requirements.aisimulate.txt index 7c9c5210f7fe..0b7ad1d4e18d 100644 --- a/container/deps/requirements.aisimulate.txt +++ b/container/deps/requirements.aisimulate.txt @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# Published AISimulate wheel staged into planner and framework runtime wheelhouses. -# Keep in sync with the aisimulate-core pins in Cargo.toml and lib/bindings/python/Cargo.toml. -aisimulate==0.13.0.dev202610290000000087 +# Source-built AISim wheel staged into all release wheelhouses. +# Keep in sync with the aisim-core pins in Cargo.toml and lib/bindings/python/Cargo.toml. +aisim==0.14.0 diff --git a/container/templates/dynamo_runtime.Dockerfile b/container/templates/dynamo_runtime.Dockerfile index 283a20e6b2a6..909d5a125c16 100644 --- a/container/templates/dynamo_runtime.Dockerfile +++ b/container/templates/dynamo_runtime.Dockerfile @@ -117,6 +117,7 @@ RUN --mount=type=cache,id=uv-dynamo-{{ context.dynamo.uv_version }},target=/home export UV_CACHE_DIR=/home/dynamo/.cache/uv && \ uv pip install \ /opt/dynamo/wheelhouse/ai_dynamo_runtime*.whl \ + /opt/dynamo/wheelhouse/aisim-*.whl \ /opt/dynamo/wheelhouse/ai_dynamo*any.whl \ /opt/dynamo/wheelhouse/nixl/nixl*.whl {% else %} diff --git a/container/templates/frontend.Dockerfile b/container/templates/frontend.Dockerfile index b12393b5c79e..f5f2e55eb194 100644 --- a/container/templates/frontend.Dockerfile +++ b/container/templates/frontend.Dockerfile @@ -175,6 +175,7 @@ RUN --mount=type=bind,source=./container/deps/overrides.frontend.txt,target=/tmp uv pip install \ --overrides /tmp/overrides.frontend.txt \ /opt/dynamo/wheelhouse/ai_dynamo_runtime*.whl \ + /opt/dynamo/wheelhouse/aisim-*.whl \ /opt/dynamo/wheelhouse/ai_dynamo*any.whl && \ # The meta package requires both backends unconditionally (its cu12/cu13 # extras are vestigial), so install the backend first and the meta module diff --git a/container/templates/planner.Dockerfile b/container/templates/planner.Dockerfile index 32b537cd408a..4bf8d3f98a00 100644 --- a/container/templates/planner.Dockerfile +++ b/container/templates/planner.Dockerfile @@ -83,7 +83,7 @@ RUN --mount=type=bind,source=./container/deps/requirements.planner.txt,target=/t --requirement /tmp/requirements.benchmark.txt \ /opt/dynamo/wheelhouse/ai_dynamo_runtime*.whl \ /opt/dynamo/wheelhouse/ai_dynamo*any.whl \ - /opt/dynamo/wheelhouse/aisimulate*.whl + /opt/dynamo/wheelhouse/aisim-*.whl # Copy only the subset of the repository needed for planner/profiler service # startup and the component-local planner-family test suites. AI Simulate diff --git a/container/templates/sglang_runtime.Dockerfile b/container/templates/sglang_runtime.Dockerfile index 159b5a5bc732..f7960f097dd9 100644 --- a/container/templates/sglang_runtime.Dockerfile +++ b/container/templates/sglang_runtime.Dockerfile @@ -128,7 +128,7 @@ COPY --chmod=775 --chown=dynamo:0 --from=wheel_builder /opt/dynamo/dist/*.whl /o RUN pip install --no-deps \ /opt/dynamo/wheelhouse/ai_dynamo_runtime*.whl \ /opt/dynamo/wheelhouse/ai_dynamo*any.whl \ - /opt/dynamo/wheelhouse/aisimulate*.whl \ + /opt/dynamo/wheelhouse/aisim-*.whl \ /opt/dynamo/wheelhouse/nixl/nixl*.whl \ "distro==1.9.0" {% else %} @@ -137,7 +137,7 @@ RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \ pip install --break-system-packages --no-deps \ /opt/dynamo/wheelhouse/ai_dynamo_runtime*.whl \ /opt/dynamo/wheelhouse/ai_dynamo*any.whl \ - /opt/dynamo/wheelhouse/aisimulate*.whl + /opt/dynamo/wheelhouse/aisim-*.whl # Install accelerate for diffusion/video worker pipelines (diffusers requires it # for enable_model_cpu_offload but the upstream SGLang runtime image omits it) diff --git a/container/templates/triton_runtime.Dockerfile b/container/templates/triton_runtime.Dockerfile index a689833914e1..6e550e219bee 100644 --- a/container/templates/triton_runtime.Dockerfile +++ b/container/templates/triton_runtime.Dockerfile @@ -130,6 +130,7 @@ RUN --mount=type=cache,target=/root/.cache/uv,sharing=locked \ uv pip install \ certifi==2026.10.23 pip==26.2.2 \ /opt/dynamo/wheelhouse/ai_dynamo_runtime*.whl \ + /opt/dynamo/wheelhouse/aisim-*.whl \ /opt/dynamo/wheelhouse/ai_dynamo*any.whl && \ \ # Triton's Python bindings (from the release image) and the tritonclient gRPC client. diff --git a/container/templates/trtllm_runtime.Dockerfile b/container/templates/trtllm_runtime.Dockerfile index a0267c078259..d89a125b0cfa 100644 --- a/container/templates/trtllm_runtime.Dockerfile +++ b/container/templates/trtllm_runtime.Dockerfile @@ -311,7 +311,7 @@ RUN --mount=type=cache,id=uv-root-{{ context.dynamo.uv_version }},target=/root/. # Dynamo's own wheels — --no-deps preserves upstream's solve. uv pip install --no-deps /opt/dynamo/wheelhouse/ai_dynamo_runtime*.whl && \ uv pip install --no-deps /opt/dynamo/wheelhouse/ai_dynamo*any.whl && \ - uv pip install --no-deps /opt/dynamo/wheelhouse/aisimulate*.whl && \ + uv pip install --no-deps /opt/dynamo/wheelhouse/aisim-*.whl && \ \ # nixl/nixl-cu13 for `import nixl` (dynamo.nixl_connect); version from NIXL_REF. # LD_PRELOAD swaps the runtime .so (nixl#1668). diff --git a/container/templates/vllm_runtime.Dockerfile b/container/templates/vllm_runtime.Dockerfile index 76ce78dd2373..77d85ed7131b 100644 --- a/container/templates/vllm_runtime.Dockerfile +++ b/container/templates/vllm_runtime.Dockerfile @@ -240,7 +240,7 @@ RUN --mount=type=cache,id=uv-root-{{ context.dynamo.uv_version }},target=/root/. export UV_CACHE_DIR=/root/.cache/uv && \ uv pip install {{ pip_target }} --no-deps /opt/dynamo/wheelhouse/ai_dynamo_runtime*.whl && \ uv pip install {{ pip_target }} --no-deps /opt/dynamo/wheelhouse/ai_dynamo*any.whl && \ - uv pip install {{ pip_target }} --no-deps /opt/dynamo/wheelhouse/aisimulate*.whl && \ + uv pip install {{ pip_target }} --no-deps /opt/dynamo/wheelhouse/aisim-*.whl && \ if [ "${ENABLE_GPU_MEMORY_SERVICE}" = "true" ]; then \ GMS_WHEEL=$(ls /opt/dynamo/wheelhouse/gpu_memory_service*.whl 2>/dev/null | head -1); \ if [ -n "$GMS_WHEEL" ]; then uv pip install {{ pip_target }} --no-deps "$GMS_WHEEL"; fi; \ diff --git a/container/templates/wheel_builder.Dockerfile b/container/templates/wheel_builder.Dockerfile index eee58ef57543..6ca104f8c46d 100644 --- a/container/templates/wheel_builder.Dockerfile +++ b/container/templates/wheel_builder.Dockerfile @@ -684,24 +684,23 @@ COPY examples/router/custom-policy-example/ /opt/dynamo/examples/router/custom-p COPY deploy/inference-gateway/ext-proc/ /opt/dynamo/deploy/inference-gateway/ext-proc/ COPY deploy/inference-gateway/sidecar/ /opt/dynamo/deploy/inference-gateway/sidecar/ -{% if target == "planner" or (target == "runtime" and framework in ("vllm", "sglang", "trtllm")) %} -COPY container/deps/requirements.aisimulate.txt /opt/dynamo/container/deps/requirements.aisimulate.txt - -# AISimulate is released separately as an abi3 wheel. Stage the pinned wheel -# directly from NVIDIA's package index. Download only this -# distribution; runtime images own dependency installation through their -# requirements files and local wheels. -RUN --mount=type=cache,id=uv-root-{{ context.dynamo.uv_version }},target=/root/.cache/uv,sharing=shared \ - export UV_CACHE_DIR=/root/.cache/uv && \ +# Build Python and Rust AISimulate from the same source revision. The published +# Dynamo metadata keeps a version requirement so its wheel can upload to PyPI. +COPY container/deps/build_aisimulate.py /opt/dynamo/container/deps/build_aisimulate.py +RUN --mount=type=secret,id=aws-web-identity-token,target=/run/secrets/aws-token \ + --mount=type=secret,id=aws-role-arn,env=AWS_ROLE_ARN \ + --mount=type=cache,target=/usr/local/cargo/registry,sharing=shared \ + --mount=type=cache,target=/usr/local/cargo/git,sharing=shared \ + export AWS_WEB_IDENTITY_TOKEN_FILE=/run/secrets/aws-token && \ + export SCCACHE_S3_KEY_PREFIX=${SCCACHE_S3_KEY_PREFIX:-${TARGETARCH}} && \ + if [ "$USE_SCCACHE" = "true" ]; then \ + eval $(/tmp/use-sccache.sh setup-env); \ + fi && \ source ${VIRTUAL_ENV}/bin/activate && \ - python -m pip download \ - --only-binary=:all: \ - --no-deps \ - --no-index \ - --find-links https://pypi.nvidia.com/aisimulate/ \ - --dest /opt/dynamo/dist \ - --requirement /opt/dynamo/container/deps/requirements.aisimulate.txt -{% endif %} + python /opt/dynamo/container/deps/build_aisimulate.py \ + --root /opt/dynamo \ + --source /opt/aisimulate-source \ + --output /opt/dynamo/dist # Compliance: harvest each crate's real LICENSE files from the cargo registry # source cache so the rust NOTICES generator can inline upstream license text @@ -734,7 +733,7 @@ RUN --mount=type=cache,target=/root/.cargo/registry,sharing=shared \ # the bare system python3 lacks it and the step would no-op with a warning. COPY container/compliance /opt/compliance RUN set -u; injected=0; \ - for whl in /opt/dynamo/dist/ai_dynamo_runtime*.whl /opt/dynamo/dist/aisimulate*.whl; do \ + for whl in /opt/dynamo/dist/ai_dynamo_runtime*.whl /opt/dynamo/dist/aisim-*.whl; do \ [ -e "$whl" ] || continue; \ PYTHONPATH=/opt ${VIRTUAL_ENV}/bin/python3 -m compliance.bundle_wheel_notices \ --wheel "$whl" --licenses-dir /opt/dynamo/rust-licenses -v \ diff --git a/deploy/operator/hack/test_aisimulate_hardware_catalog.py b/deploy/operator/hack/test_aisimulate_hardware_catalog.py index d4bd24083524..00fcb33df1cf 100644 --- a/deploy/operator/hack/test_aisimulate_hardware_catalog.py +++ b/deploy/operator/hack/test_aisimulate_hardware_catalog.py @@ -38,7 +38,7 @@ def _pinned_version() -> Version: requirements = [ Requirement(dependency) for dependency in dependencies - if canonicalize_name(Requirement(dependency).name) == "aisimulate" + if canonicalize_name(Requirement(dependency).name) == "aisim" ] assert len(requirements) == 1, "pyproject.toml must contain one AISimulate pin" @@ -50,7 +50,7 @@ def _pinned_version() -> Version: def _packaged_hardware_skus() -> list[str]: - distribution = metadata.distribution("aisimulate") + distribution = metadata.distribution("aisim") system_files = [ path for path in distribution.files or [] @@ -86,7 +86,7 @@ def _operator_hardware_skus() -> list[str]: def test_dgdr_hardware_catalog_matches_pinned_aisimulate() -> None: - assert Version(metadata.version("aisimulate")) == _pinned_version() + assert Version(metadata.version("aisim")) == _pinned_version() expected = _packaged_hardware_skus() assert _operator_hardware_skus() == expected diff --git a/deploy/operator/internal/controller/testdata/dgdr/deployment/output.yaml b/deploy/operator/internal/controller/testdata/dgdr/deployment/output.yaml index eda91f765e92..475abd7baa8d 100644 --- a/deploy/operator/internal/controller/testdata/dgdr/deployment/output.yaml +++ b/deploy/operator/internal/controller/testdata/dgdr/deployment/output.yaml @@ -39,7 +39,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size @@ -78,7 +78,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size @@ -131,7 +131,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size @@ -201,7 +201,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size @@ -482,7 +482,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size @@ -656,7 +656,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size diff --git a/deploy/operator/internal/controller/testdata/dgdr/grove/output.yaml b/deploy/operator/internal/controller/testdata/dgdr/grove/output.yaml index f7924e1ea573..47ababa70638 100644 --- a/deploy/operator/internal/controller/testdata/dgdr/grove/output.yaml +++ b/deploy/operator/internal/controller/testdata/dgdr/grove/output.yaml @@ -41,7 +41,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size @@ -81,7 +81,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size @@ -237,7 +237,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size @@ -373,7 +373,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size diff --git a/deploy/operator/internal/controller/testdata/dgdr/lws/output.yaml b/deploy/operator/internal/controller/testdata/dgdr/lws/output.yaml index ab9902f71983..3fd75991d8f1 100644 --- a/deploy/operator/internal/controller/testdata/dgdr/lws/output.yaml +++ b/deploy/operator/internal/controller/testdata/dgdr/lws/output.yaml @@ -41,7 +41,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size @@ -80,7 +80,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size @@ -135,7 +135,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size @@ -311,7 +311,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size @@ -443,7 +443,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size @@ -601,7 +601,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size @@ -775,7 +775,7 @@ spec: - --ais-backend - vllm - --ais-backend-version - - 0.24.0 + - 0.30.0 - --ais-system - h100_sxm - --ais-tp-size diff --git a/lib/bindings/kvbm/Cargo.lock b/lib/bindings/kvbm/Cargo.lock index cde62358db63..c7351243cb28 100644 --- a/lib/bindings/kvbm/Cargo.lock +++ b/lib/bindings/kvbm/Cargo.lock @@ -33,10 +33,9 @@ dependencies = [ ] [[package]] -name = "aisimulate-core" -version = "0.13.0-dev.202610290000000087" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb823782e806dd74cba719906a6b828b86f415aad54c6d5016c29ff278e89319" +name = "aisim-core" +version = "0.14.0" +source = "git+https://github.com/ai-dynamo/aisimulate.git?rev=74dcdfcde184070d39e813ba24424b32e9627f00#74dcdfcde184070d39e813ba24424b32e9627f00" dependencies = [ "anyhow", "bincode 1.3.3", @@ -2277,7 +2276,7 @@ dependencies = [ name = "dynamo-mocker" version = "1.7.0" dependencies = [ - "aisimulate-core", + "aisim-core", "anyhow", "blake3", "bytes", diff --git a/lib/bindings/kvbm/Cargo.toml b/lib/bindings/kvbm/Cargo.toml index d64242bb3446..40c6bbf7e67f 100644 --- a/lib/bindings/kvbm/Cargo.toml +++ b/lib/bindings/kvbm/Cargo.toml @@ -60,3 +60,7 @@ dlpark = { version = "0.5", features = ["pyo3", "half"], optional = true } cudarc = { version = "=0.19.8", features = ["cuda-version-from-build-system", "fallback-latest"], optional = true } [dev-dependencies] + +# Release-only AISim 0.14.0 RC0 source pin for Dynamo 1.7.0. +[patch.crates-io] +aisim-core = { git = "https://github.com/ai-dynamo/aisimulate.git", rev = "74dcdfcde184070d39e813ba24424b32e9627f00" } diff --git a/lib/bindings/python/Cargo.lock b/lib/bindings/python/Cargo.lock index 41222905bd9e..d89892da2e00 100644 --- a/lib/bindings/python/Cargo.lock +++ b/lib/bindings/python/Cargo.lock @@ -33,10 +33,9 @@ dependencies = [ ] [[package]] -name = "aisimulate-core" -version = "0.13.0-dev.202610290000000087" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb823782e806dd74cba719906a6b828b86f415aad54c6d5016c29ff278e89319" +name = "aisim-core" +version = "0.14.0" +source = "git+https://github.com/ai-dynamo/aisimulate.git?rev=74dcdfcde184070d39e813ba24424b32e9627f00#74dcdfcde184070d39e813ba24424b32e9627f00" dependencies = [ "anyhow", "bincode 1.3.3", @@ -2340,7 +2339,7 @@ dependencies = [ name = "dynamo-mocker" version = "1.7.0" dependencies = [ - "aisimulate-core", + "aisim-core", "anyhow", "blake3", "bytes", @@ -2433,7 +2432,7 @@ dependencies = [ name = "dynamo-py3" version = "1.7.0" dependencies = [ - "aisimulate-core", + "aisim-core", "anyhow", "async-stream", "async-trait", diff --git a/lib/bindings/python/Cargo.toml b/lib/bindings/python/Cargo.toml index fca3c27b40d9..97a8aaef78aa 100644 --- a/lib/bindings/python/Cargo.toml +++ b/lib/bindings/python/Cargo.toml @@ -61,7 +61,7 @@ request-trace-s3 = ["dynamo-llm/request-trace-s3"] # former AIC crate surface. The `python` feature is required for # AicEngineBuilder; this extension supplies the interpreter, so the dependency # must not enable `extension-module`. -aisimulate-core = { version = "=0.13.0-dev.202610290000000087", optional = true, features = ["python"] } +aisimulate-core = { package = "aisim-core", version = "=0.14.0", optional = true, features = ["python"] } dynamo-runtime = { path = "../../runtime" } dynamo-mocker = { path = "../../mocker" } dynamo-parsers = { version = "=10.2.2" } @@ -142,3 +142,7 @@ tracing-subscriber = { version = "0.3", features = ["registry"] } inherits = "release" debug = true strip = false + +# Release-only AISim 0.14.0 RC0 source pin for Dynamo 1.7.0. +[patch.crates-io] +aisim-core = { git = "https://github.com/ai-dynamo/aisimulate.git", rev = "74dcdfcde184070d39e813ba24424b32e9627f00" } diff --git a/lib/bindings/python/tests/replay/test_replay_telemetry.py b/lib/bindings/python/tests/replay/test_replay_telemetry.py index 78c94a51af0f..e3386532d450 100644 --- a/lib/bindings/python/tests/replay/test_replay_telemetry.py +++ b/lib/bindings/python/tests/replay/test_replay_telemetry.py @@ -99,7 +99,10 @@ def test_telemetry_capture_callback_and_jsonl_share_one_sample_stream( assert sum(sample["traffic"]["arriving_requests"] for sample in samples) == 6 assert sum(sample["traffic"]["completed_requests"] for sample in samples) == 6 + assert all(sample["host_caches"] == [] for sample in samples) + assert set(samples[0]) == { + "host_caches", "sample_ordinal", "kind", "interval_start_ms", diff --git a/lib/mocker/src/live/tests.rs b/lib/mocker/src/live/tests.rs index df259dc66037..1e6897dfd1a4 100644 --- a/lib/mocker/src/live/tests.rs +++ b/lib/mocker/src/live/tests.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; use std::time::Duration; use super::*; @@ -13,14 +13,19 @@ use dynamo_kv_router::protocols::StorageTier; struct NoopKvSink; #[derive(Default)] -struct CountingFpmSink(AtomicUsize); +struct CountingFpmSink { + passes: AtomicUsize, + decode_requests: AtomicU32, +} impl FpmSink for CountingFpmSink { fn publish( &self, - _snapshot: crate::common::protocols::ForwardPassSnapshot, + snapshot: crate::common::protocols::ForwardPassSnapshot, ) -> anyhow::Result<()> { - self.0.fetch_add(1, Ordering::Relaxed); + self.passes.fetch_add(1, Ordering::Relaxed); + self.decode_requests + .fetch_add(snapshot.num_decode_requests, Ordering::Relaxed); Ok(()) } } @@ -236,7 +241,7 @@ async fn assert_mtp_lifecycle_drains_through_live_boundary(engine_type: EngineTy } wait_for_idle(&engine).await; - let passes = fpm.0.load(Ordering::Relaxed); + let passes = fpm.passes.load(Ordering::Relaxed); assert!(passes > 0); assert!( passes < 7, @@ -1000,14 +1005,14 @@ async fn full_output_stream_is_cancelled_without_stalling_an_unrelated_request() .unwrap(); assert_eq!(fast_output.token_id, Some(22)); assert!(fast_output.completed); + wait_for_idle(&engine).await; assert_eq!(slow.recv().await.unwrap().token_id, Some(7)); assert!(slow.recv().await.is_none()); assert!(!slow.is_aborted()); - wait_for_idle(&engine).await; assert_eq!( - fpm.0.load(Ordering::Relaxed), - 2, - "a full route must be cancelled at its completion boundary before a third pass starts" + fpm.decode_requests.load(Ordering::Relaxed), + 1, + "a full route must be cancelled before the slow request decodes again" ); } diff --git a/pyproject.toml b/pyproject.toml index 5cc9200cc66a..077980b3fdf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ license-files = ["LICENSE"] requires-python = ">=3.11" dependencies = [ "ai-dynamo-runtime==1.7.0", - "aisimulate==0.13.0.dev202610290000000087 ; python_version >= '3.11' and python_version < '3.14'", + "aisim==0.14.0 ; python_version >= '3.11' and python_version < '3.14'", "aiohttp>=3.14.3,<4.0", # Set the CA bundle floor for transitive HTTP clients outside containers too. "certifi>=2026.10.23", diff --git a/tests/dependencies/test_aisimulate_consistency.py b/tests/dependencies/test_aisimulate_consistency.py index f9e1b4ff2367..eca3f964e5af 100644 --- a/tests/dependencies/test_aisimulate_consistency.py +++ b/tests/dependencies/test_aisimulate_consistency.py @@ -1,11 +1,13 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Keep Dynamo on one published AISimulate release.""" +"""Keep Dynamo on one source-pinned AISim release.""" from __future__ import annotations import re +import runpy +import subprocess import sys import tomllib from importlib import metadata @@ -17,7 +19,7 @@ from packaging.utils import canonicalize_name from packaging.version import Version -from tests.wheels.smoke_install import AISIMULATE_FIND_LINKS +from tests.wheels import smoke_install pytestmark = [ pytest.mark.gpu_0, @@ -48,7 +50,7 @@ def _python_requirement(pyproject: dict) -> Requirement: matches = [ Requirement(requirement) for requirement in pyproject["project"]["dependencies"] - if canonicalize_name(Requirement(requirement).name) == "aisimulate" + if canonicalize_name(Requirement(requirement).name) == "aisim" ] assert len(matches) == 1, "ai-dynamo must declare one AISimulate dependency" return matches[0] @@ -61,38 +63,36 @@ def _requirements_file_aisimulate_requirement(path: Path) -> Requirement: if not requirement or requirement.startswith("--"): continue parsed = Requirement(requirement) - if canonicalize_name(parsed.name) == "aisimulate": + if canonicalize_name(parsed.name) == "aisim": matches.append(parsed) assert len(matches) == 1, f"{path} must declare one AISimulate dependency" return matches[0] def _exact_version(requirement: Requirement) -> Version: - assert requirement.url is None, "AISimulate must resolve from PyPI" + assert requirement.url is None, "AISim must declare a version requirement" specifiers = list(requirement.specifier) assert ( len(specifiers) == 1 and specifiers[0].operator == "==" - ), "AISimulate must use one exact PyPI version" + ), "AISim must use one exact version" return Version(specifiers[0].version) -def _locked_cargo_version(path: Path) -> Version: +def _locked_cargo_version(path: Path, patch: dict) -> Version: with path.open("rb") as handle: packages = tomllib.load(handle)["package"] - matches = [package for package in packages if package["name"] == "aisimulate-core"] - assert len(matches) == 1, f"expected one aisimulate-core package in {path}" + matches = [package for package in packages if package["name"] == "aisim-core"] + assert len(matches) == 1, f"expected one aisim-core package in {path}" package = matches[0] assert package.get("source") == ( - "registry+https://github.com/rust-lang/crates.io-index" - ), f"aisimulate-core must resolve from crates.io in {path}" - assert re.fullmatch( - r"[0-9a-f]{64}", str(package.get("checksum", "")) - ), f"aisimulate-core must have a registry checksum in {path}" + f"git+{patch['git']}?rev={patch['rev']}#{patch['rev']}" + ), f"aisim-core must resolve from the exact RC source in {path}" + assert "checksum" not in package return Version(str(package["version"])) -def test_dynamo_pins_matching_published_aisimulate_releases() -> None: +def test_dynamo_pins_matching_aisim_rc_sources() -> None: pyproject, cargo = _root_configs() python_requirement = _python_requirement(pyproject) python_version = _exact_version(python_requirement) @@ -117,16 +117,12 @@ def test_dynamo_pins_matching_published_aisimulate_releases() -> None: with (ROOT / "benchmarks/pyproject.toml").open("rb") as handle: benchmarks = tomllib.load(handle) assert _exact_version(_python_requirement(benchmarks)) == python_version - assert "aisimulate" not in pyproject.get("tool", {}).get("uv", {}).get( - "sources", {} - ) + assert "aisim" not in pyproject.get("tool", {}).get("uv", {}).get("sources", {}) cargo_dependency = cargo["workspace"]["dependencies"]["aisimulate-core"] assert not {"path", "git", "rev", "branch", "tag"} & cargo_dependency.keys() cargo_requirement = str(cargo_dependency["version"]) - assert cargo_requirement.startswith( - "=" - ), "aisimulate-core must use one exact crates.io version" + assert cargo_requirement.startswith("="), "aisim-core must use one exact version" cargo_version = Version(cargo_requirement.removeprefix("=")) with (ROOT / "lib/bindings/python/Cargo.toml").open("rb") as handle: @@ -134,35 +130,102 @@ def test_dynamo_pins_matching_published_aisimulate_releases() -> None: bindings_dependency = bindings["dependencies"]["aisimulate-core"] assert not {"path", "git", "rev", "branch", "tag"} & bindings_dependency.keys() assert bindings_dependency["version"] == cargo_requirement - + assert cargo_dependency["package"] == bindings_dependency["package"] == "aisim-core" + + patch = cargo["patch"]["crates-io"]["aisim-core"] + assert set(patch) == {"git", "rev"} + assert patch["git"] == "https://github.com/ai-dynamo/aisimulate.git" + assert re.fullmatch(r"[0-9a-f]{40}", patch["rev"]) + for path in LOCKFILES[1:]: + with path.with_name("Cargo.toml").open("rb") as handle: + manifest = tomllib.load(handle) + assert manifest["patch"]["crates-io"]["aisim-core"] == patch assert cargo_version == python_version - assert all(_locked_cargo_version(path) == cargo_version for path in LOCKFILES) - - -def test_container_stages_the_published_aisimulate_wheel() -> None: - pyproject, _ = _root_configs() - python_version = _exact_version(_python_requirement(pyproject)) - container_version = _exact_version( - _requirements_file_aisimulate_requirement(AISIMULATE_REQUIREMENTS) - ) - wheel_builder = (ROOT / "container/templates/wheel_builder.Dockerfile").read_text( - encoding="utf-8" + assert all( + _locked_cargo_version(path, patch) == cargo_version for path in LOCKFILES ) - assert container_version == python_version - assert "requirements.aisimulate.txt" in wheel_builder - assert ( - "--requirement /opt/dynamo/container/deps/requirements.aisimulate.txt" - in wheel_builder - ) - assert "--only-binary=:all:" in wheel_builder - assert "--no-deps" in wheel_builder - assert "--no-index" in wheel_builder - assert AISIMULATE_FIND_LINKS == "https://pypi.nvidia.com/aisimulate/" - assert f"--find-links {AISIMULATE_FIND_LINKS}" in wheel_builder - assert "COPY aisimulate" not in wheel_builder - assert "/opt/dynamo/aisimulate" not in wheel_builder - assert not (ROOT / "aisimulate").exists() + +def test_container_builds_the_matching_aisim_source_wheel() -> None: + wheel_builder = (ROOT / "container/templates/wheel_builder.Dockerfile").read_text() + assert "container/deps/build_aisimulate.py" in wheel_builder + assert "--root /opt/dynamo" in wheel_builder + assert "--output /opt/dynamo/dist" in wheel_builder + assert "pip download" not in wheel_builder + assert "pypi.nvidia.com/aisimulate" not in wheel_builder + for name in ( + "planner", + "sglang_runtime", + "trtllm_runtime", + "vllm_runtime", + "frontend", + "dynamo_runtime", + "triton_runtime", + ): + template = (ROOT / f"container/templates/{name}.Dockerfile").read_text() + assert "/opt/dynamo/wheelhouse/aisim-*.whl" in template + assert "/opt/dynamo/wheelhouse/aisimulate*.whl" not in template + + +@pytest.mark.parametrize( + "name,version", + [ + ("aisim", "0.14.0"), + ("aisim", "0.14.0.dev1"), + ("aisim", "0.15.0"), + ("aisimulate", "0.14.0"), + ], +) +def test_source_wheel_build_validates_identity_before_compilation( + tmp_path, monkeypatch, name, version +): + _, cargo = _root_configs() + patch = cargo["patch"]["crates-io"]["aisim-core"] + source, output = tmp_path / "source", tmp_path / "wheels" + commands = [] + + def run(command, **kwargs): + commands.append(command) + if "checkout" in command: + project = source / "python/aisimulate" + project.mkdir(parents=True) + (project / "pyproject.toml").write_text( + f'[project]\nname = "{name}"\nversion = "{version}"\n' + ) + return subprocess.CompletedProcess(command, 0) + + monkeypatch.setattr(subprocess, "run", run) + build = runpy.run_path(str(ROOT / "container/deps/build_aisimulate.py"))["build"] + if (name, version) == ("aisim", "0.14.0"): + build(ROOT, source, output) + assert commands[1] == [ + "git", + "-C", + str(source), + "fetch", + "--depth=1", + patch["git"], + patch["rev"], + ] + assert commands[-1][0] == "maturin" + assert "--locked" in commands[-1] + else: + with pytest.raises(ValueError): + build(ROOT, source, output) + assert all(command[0] != "maturin" for command in commands) + + +def test_wheel_smoke_requires_the_source_built_aisim_wheel(tmp_path, monkeypatch): + commands = [] + monkeypatch.setattr(smoke_install, "run", lambda command: commands.append(command)) + with pytest.raises(AssertionError, match="missing required 'aisim'"): + smoke_install.pip_install(Path("python"), tmp_path, ["ai-dynamo"]) + wheel = tmp_path / "aisim-0.14.0-cp311-abi3-manylinux_2_28_x86_64.whl" + wheel.touch() + smoke_install.pip_install(Path("python"), tmp_path, ["ai-dynamo"]) + assert str(wheel) in commands[0] + assert "aisim" in commands[0] + assert not any("pypi.nvidia.com" in arg for arg in commands[0]) def test_planner_ci_image_collects_unified_cli_e2e_tests() -> None: @@ -181,7 +244,7 @@ def test_installed_aisimulate_matches_the_declared_release() -> None: pyproject, _ = _root_configs() expected = _exact_version(_python_requirement(pyproject)) - assert Version(metadata.version("aisimulate")) == expected + assert Version(metadata.version("aisim")) == expected def test_ai_dynamo_registers_only_its_aisimulate_providers() -> None: diff --git a/tests/dependencies/test_aisimulate_namespaces.py b/tests/dependencies/test_aisimulate_namespaces.py index 2dd733993e89..d8f234de26ec 100644 --- a/tests/dependencies/test_aisimulate_namespaces.py +++ b/tests/dependencies/test_aisimulate_namespaces.py @@ -26,7 +26,12 @@ ] ROOT = Path(__file__).resolve().parents[2] -LEGACY_DISTRIBUTIONS = {"aiconfigurator", "aiconfigurator-core"} +LEGACY_DISTRIBUTIONS = { + "aiconfigurator", + "aiconfigurator-core", + "aisimulate", + "aisimulate-core", +} CARGO_LOCKFILES = ( ROOT / "Cargo.lock", ROOT / "lib/bindings/python/Cargo.lock", @@ -147,7 +152,7 @@ def test_no_manifest_installs_retired_aic_distributions() -> None: requirement = next( Requirement(item) for item in root_project["dependencies"] - if canonicalize_name(Requirement(item).name) == "aisimulate" + if canonicalize_name(Requirement(item).name) == "aisim" ) if "git" in canonical_core: assert requirement.url == ( @@ -165,7 +170,7 @@ def test_aisimulate_wheel_uses_canonical_import_namespaces() -> None: if sys.version_info < (3, 11) or sys.version_info >= (3, 14): pytest.skip("AISimulate supports Python 3.11 through 3.13") - release = metadata.distribution("aisimulate") + release = metadata.distribution("aisim") release_requirements = _requirement_names(release.requires or []) release_files = {str(path) for path in release.files or []} diff --git a/tests/wheels/smoke_install.py b/tests/wheels/smoke_install.py index b407723357af..a50f639bdfe6 100644 --- a/tests/wheels/smoke_install.py +++ b/tests/wheels/smoke_install.py @@ -256,10 +256,6 @@ def create_venv(python_spec: str) -> Path: return venv_dir / "bin" / "python" -# Stage the pinned AISimulate wheel directly from NVIDIA's package index. -AISIMULATE_FIND_LINKS = "https://pypi.nvidia.com/aisimulate/" - - def pip_install(venv_python: Path, wheelhouse: Path, requirements: list[str]) -> None: # --find-links only registers search paths; nixl is pulled from nixl/ only when a # requirement resolves to it, not installed on its own. @@ -267,12 +263,9 @@ def pip_install(venv_python: Path, wheelhouse: Path, requirements: list[str]) -> for path in (wheelhouse, wheelhouse / "nixl"): if path.exists(): find_links.extend(["--find-links", str(path)]) - # Planner and framework runtime wheelhouses stage their own aisimulate wheel. Offer - # NVIDIA's index only when that staged wheel is absent: pip ranks equal candidates - # without regard to where they came from, so registering both risks installing the - # remote copy in place of the artifact the image actually ships. - if not find_wheels(wheelhouse, "aisimulate"): - find_links.extend(["--find-links", AISIMULATE_FIND_LINKS]) + # Every release wheelhouse carries AISim built from the pinned RC source. + # Install that exact wheel rather than consulting a public package index. + ais_wheel = require_one_wheel(wheelhouse, "aisim") run( [ str(venv_python), @@ -280,8 +273,9 @@ def pip_install(venv_python: Path, wheelhouse: Path, requirements: list[str]) -> "pip", "install", "--only-binary", - "aisimulate", + "aisim", *find_links, + str(ais_wheel), *requirements, ] )